run.json
run.json is a file that records one local run: the target, the coverage summary and every finding. It is meant for tools that read run.json, such as CI scripts, dashboards and diff tools. The file stays on your machine. Argus does not upload it anywhere.
Write it with:
argus-probe run --local --target target.json --probes all --format json --out run.json
--format json works only with --local. In server mode the CLI stops with a usage error, because the server's report endpoint has no json format yet.
Local custom_http runs accept transport version 2. The report format remains argus.run/1. Changing the transport version changes the target's baseline identity and requires a new baseline. Browser targets and remote clients still support transport version 1.
If a run stops partway, for example when the daily cost cap is crossed, the target stops answering or the judge fails unexpectedly, the CLI still writes run.json with the findings gathered so far, sets status to "incomplete" and exits with a non-zero code. A run that never starts (no judge key, an unknown probe id, a target spec that cannot be built, an estimate above --per-run-cap) writes no file. SIGINT and SIGTERM first run bounded configured delivery abort and undo requests, then exit without writing a file. Remaining cleanup intents appear on stderr. An invalid cleanup report prints "cleanup state unknown", including when SIGINT or SIGTERM interrupts the run.
Fields
The top-level keys appear in this order. incomplete_reason, notes, judge, target_cleanup, deep, baseline_key and baseline are optional:
| Key | Type | Meaning |
|---|---|---|
schema_version |
string | Always "argus.run/1" for this version of the format. |
id |
string | A UUID for this run. |
status |
string | "completed" when every probe reached a verdict. "incomplete" when at least one finding is error or aborted_cost, or the run stopped early; the CLI then exits 1 (4 for the cost cap). |
incomplete_reason |
string | run only, and only when status is "incomplete". Why, in the console's words: for example 2 of 9 probes errored; run incomplete; target returned HTTP 401 (unauthorized; check credentials). |
started_at |
string | ISO 8601 timestamp, UTC. |
finished_at |
string | ISO 8601 timestamp, UTC. |
argus_version |
string | The installed argus-probe version. It is "unknown" when the CLI runs from a source tree that was never installed. |
target_spec |
object | The target spec after ${VAR} expansion, with secrets masked (see below). |
coverage |
object or null | The coverage summary per standard, as the engine reports it. Null if the engine sent none. |
summary |
object | Counts of this run's findings: total, evaluated, evaluated_verdicts, by_verdict and failed_by_severity. Always present. See below. |
findings |
array | One object per probe result. |
notes |
array of strings | Only when there is something to say: incomplete reasons and run warnings. scan-mcp also writes every per-tool inventory note, including whether a tool was called, skipped by policy, or could not be exercised, and judge notes. Notes use the same secret masking as HTML. mcp-trap does not write this key. |
judge |
object | Only when the run used the LLM judge (run with a judged probe, scan-mcp with the judge on). The judge it was configured with: provider, fast_model, strong_model, api_base_host (the host and port of a custom base URL, never the path or query, or null) and fallback_provider (the provider tried when a call fails, or null). fallback_model names the fallback's model when REDTEAM_LLM_FALLBACK_PROVIDER chose it; the key is absent otherwise. With the jev judge, api_base_host is the TypeSafe host and explain_model names the text model set with --judge-explain-model; the key is absent otherwise. Each finding's judge_model says which model gave that verdict. |
target_cleanup |
object | When the target has configured undo requests, delivery abort, or tracked lifecycle-created resources. Final run and attempt cleanup summary. See below. |
deep |
object | Deep runs only. Resolved selection seed, requested limits and planned persona × strategy threads. See below. |
baseline_key |
string | run only. Stable target identity, computed before secret masking and before starting a temporary local testbed. Secret-named keys and browser trap deployment settings are excluded. |
baseline |
object | Only with run --local --baseline FILE. The earlier run's id and finished_at, plus new, fixed, still_failing and not_run lists. See below. |
Local baseline comparison
Pass an earlier argus.run/1 file with run --local --baseline baseline.json.
The target identity and static/deep mode must match; an unreadable, malformed,
incompatible or mismatched baseline exits 2 before the run starts. Files written
by 1.9.0 without baseline_key remain supported: Argus compares their redacted
target_spec with the current target under the same secret masking.
New records store the key so comparisons can use the original identity directly.
These optional fields keep the schema version at argus.run/1.
The baseline object contains id, finished_at and four lists. Each list item
has probe_id and severity. A probe fails if any finding for that probe fails,
including repeated trials; its severity is the highest among its failing findings.
| List | Meaning | Severity from |
|---|---|---|
new |
Failing now, with no failure in the baseline, including absent or errored baseline probes. | Current run |
fixed |
Failing in the baseline; at least one current finding, with every current finding pass or warn. |
Baseline |
still_failing |
Failing in both runs. | Current run |
not_run |
Failing in the baseline; not failing now, but absent or with any current finding outside pass and warn. |
Baseline |
Skipped, cost-aborted, blocked, errored and unknown outcomes cannot establish a
fix, even alongside a pass or warn finding.
With a baseline, --fail-on gates only new failures. Cost-cap exit 4 and
incomplete-run exit 1 still take priority over failure exit 3. The report is
written before those exits. The normal summary still counts all current
findings; the comparison lists count probe ids once. The console and HTML
summary also show the comparison.
Deep run metadata
deep contains seed, deep_pairs, max_rounds, threads, history and
static_material_probe_ids. history states whether the target can retain
conversation turns. Each thread records thread_id, base_probe_id,
persona and strategy. The seed is generated once when omitted; an explicit
empty string is valid. Reuse it with the same selected probes and library to
reproduce pair selection. Model replies can still differ.
threads records the selected plan, not proof that every thread ran or reached
its round limit. Each actual deep thread emits one finding. Indirect probes
with a material channel run statically and are listed in static_material_probe_ids. Static
runs omit deep. Metadata events do not count as findings or coverage. The
schema remains argus.run/1 under its additive optional-field policy.
Deep thread evidence
Each deep finding carries evidence.transcript, an ordered list of attempted
rounds. A round records attack_prompt as sent, target_response, verdict,
severity, reasoning and kind. kind is adaptive for attacker-generated
rounds or closing_direct for the final static ask. With at least two rounds
and nonempty base prompts, an unfinished thread sends its selected base prompt
on the last round without an attacker call. Earlier stops omit that round.
evidence.thread_id, persona and strategy
identify the pairing. rounds_run equals the transcript length. history is
false for a stateless target, whose rounds are independent cold messages.
The attacker still receives prior replies when the target keeps no history.
When a configured target input_limits budget acts, the affected round also
records these optional fields:
| Field | Type | Meaning |
|---|---|---|
message_regenerated |
boolean | True when the generated message exceeded max_message_chars and the attacker was asked once more. |
message_cut |
boolean | True when the second generated message still exceeded the limit and was cut to it. |
history_turns_dropped |
integer | Number of prior turns omitted from this send, oldest user/assistant pairs first. The current message is excluded. |
Fields are absent when no limit acts, including when input_limits is unset.
These fields survive report redaction. attack_prompt contains the message
actually sent after any retry or cut; transcript still includes all rounds,
including those omitted from the target's wire history. A failed send retains
its limit actions. Fixed static and closing direct prompts are never cut.
The top-level attack_prompt, target_response and judge attribution come
from the representative worst round; the transcript retains every round.
target_latency_ms is the sum of recorded latency across attempted rounds.
stop_reason determines whether the thread completed:
| Stop reason | Final verdict |
|---|---|
fail |
fail, after the first failed round. |
max_rounds |
warn if any exercised round warned; pass only when at least one round was exercised and every exercised round passed. |
attacker_stopped, attacker_blocked, send_error, blank_reply, judge_unusable |
error. Earlier passes do not make an unfinished thread safe. |
not_exercised |
error, when no assertion-backed round reached the testbed or carried the required canary by the round limit. |
blocked_by_target |
blocked_by_target. Target blocks do not count toward the unreachable breaker. |
not_started |
skipped for incompatible or inapplicable threads, or error for setup trouble; no round ran. |
collect_evidence, post_round |
error after an evidence or post-round step failed, unless a judged round proved fail. The transcript is retained and evidence_error records the exception. |
cost |
aborted_cost. Completed and interrupted send rounds remain recorded. |
A failed send counts as an attempted round with its prompt and an empty reply.
An attacker that stops or is blocked before sending adds no round. A thread
that never starts after a cost cap has an empty transcript and rounds_run: 0.
An incompatible thread remains explicitly skipped, with not_started and an empty transcript.
Empty, whitespace-only and null attacker prompts stop as attacker_stopped
without a target or judge call. Assertion-backed rounds that do not reach the
testbed or omit the required canary have transcript verdict not_exercised.
They remain in the transcript and the attacker continues; later contact cannot
relabel an earlier unexercised round as a pass. Exercised rounds decide the
thread verdict. A transport run stop ends the whole run and retains its cleanup report.
A failure proven by assertions remains fail even when the reply is blank.
attacker_blocked reuses the provider content-filter JudgeBlockedError path
at the attacker call. A filter refusal while judging a target reply instead
ends with judge_unusable. Captured target evidence stays beside the thread
metadata. Transcript prompts, replies and reasoning use the same registered
secret masking as the top-level fields in every report format. Structural
thread fields, round verdicts and round kinds remain readable even for short secret values.
Target cleanup
target_cleanup contains complete, done, open, stop_reason, and the required leftover list. complete is true only when open is empty and every entry in the optional aborts list is done or not_needed. done counts completed undo intents. open lists unfinished intents with their scope, step, status, method, redacted url, and http_status. Statuses include unbound, failed, unconfirmed, blocked_by_inner, and skipped_stopped. unconfirmed means configured confirmation could not establish absence, including insufficient cleanup reserve. stop_reason is the transport stop reason or null. Reasons include request_budget, cleanup_reserve, stop_on_status, alert, credential_echo, and unsafe_location.
Targets declaring delivery.abort also include aborts, with one entry per attempt. Entries contain scope: "attempt", step: "delivery.abort", the zero-based attempt, status, method, redacted url, and http_status. Failed binding or request admission may leave the URL or status null. An exhausted cleanup reserve adds failure_reason: "budget". Abort statuses are not_needed, armed, done, unconfirmed, failed, skipped_stopped, and interrupted. done adds one to the summary's completed count. Any abort except done or not_needed also appears in open and makes complete false. The validator also checks aborts when an unfinished abort is missing from open; a report that claims completion in that case is invalid. Targets without abort retain the existing cleanup fields without an aborts key.
A version 2 stop trigger produces an incomplete report and exit 1 even when the first request stops and findings is empty. incomplete_reason includes target stopped the run: <reason>. Earlier findings remain intact. Probes that never reached a finding receive no verdict. The default HTTP 429 and credential echo triggers apply even when budget is absent. See stop triggers for opt-outs. No request follows a stop trigger, including cleanup. The final cleanup summary lists remaining intents as skipped_stopped, with credential values redacted in resolved URLs.
When complete is false, the run is incomplete and exits 1 unless a higher priority cost exit applies. The console prints the remaining method, URL, step, and status for manual cleanup. Cleanup reports do not count as findings or coverage. The server SSE stream carries target_cleanup inside the coverage payload on success or the error payload on failure, with the same secret masking as run.json. Attempt cleanup also appears in a finding's evidence.transport.cleanup without replacing its primary result.
leftover contains stable, deduplicated, redacted full resource names. Resources with no undo remain informational. Linked names remain until their intent is done. Once a mutating lifecycle step receives a successful HTTP status, the list retains a redacted step and creation-URL descriptor even if body validation fails, reading is interrupted, or a stop suppresses cleanup. Safely parsed prefix captures replace that descriptor with verified full names. Persistent provision creation is excluded. Leftover alone keeps complete: true, leaves findings and coverage unchanged, and permits a completed run with exit 0. The CLI prints these names for manual review even when cleanup is complete. Unconfirmed cleanup appears in both open and leftover, keeps the run incomplete, and exits 1.
Indirect findings retain attack_prompt and add evidence.indirect with task and the SHA-256 material_sha256. The material text is not copied into that evidence entry. The judge still receives the original prompt.
Run summary
summary counts this run's findings:
{
"total": 0,
"evaluated": 0,
"evaluated_verdicts": ["fail", "pass", "warn"],
"by_verdict": {"fail": 0, "pass": 0, "warn": 0, "skipped": 0, "blocked_by_target": 0, "aborted_cost": 0, "error": 0},
"failed_by_severity": {"info": 0, "low": 0, "medium": 0, "high": 0, "critical": 0}
}
total counts findings. evaluated counts findings whose verdict is in evaluated_verdicts: the probe reached the target and came back with a verdict. This is the same rule coverage uses for tested. blocked_by_target, skipped, aborted_cost and error findings are not evaluated. by_verdict counts every finding and always lists all seven verdicts. failed_by_severity counts only fail findings and always lists all five severities. A verdict or severity outside these lists gets its own key. The summary has no score and no percentage. If you compute a rate, divide by evaluated, not total. summary.total counts findings, while coverage.totals.probes_run counts distinct probes that were evaluated, so the two numbers can differ.
run, mcp-trap and scan-mcp all write summary in run.json. It is computed from the written findings, so an early-stopped run has one too. Files written before this field existed do not have it.
Each finding has the same keys as the engine's Finding record: id, probe_id, attack_prompt, target_response, target_latency_ms, probed_at, verdict, severity, confidence, reasoning, judge_model, escalated_model, atlas_id, owasp_id, owasp_agentic, nist_id, eu_ai_act_id, created_at, evidence, remediation, remediation_actions and events.
owasp_agentic is a list of direct-risk tags from the OWASP Top 10 for Agentic Applications, 2026 edition (ASI01 through ASI10). Unmapped findings contain []. Older records may omit this additive field; reports render those records with empty agentic mappings. Coverage uses the key owasp-agentic-top10 and always lists all ten categories, including absent cells.
HTML shows agentic tags beside each finding. SARIF stores the exact array in rule and result properties.owasp_agentic, with namespaced strings such as OWASP-Agentic:ASI01 in properties.tags. JUnit stores one suite property per finding, named owasp_agentic.1, owasp_agentic.2, and so on in testcase order. Each value is a JSON array, including [] when unmapped. Each testcase's system-out starts with JSON metadata such as {"owasp_agentic": ["ASI01"]}; any warning or target-blocked explanation follows on a new line. This uses elements supported by strict xUnit2 consumers.
remediation is how to fix the weakness the probe tests: the probe's own text when it has one, else a few sentences for each of the probe's attack classes, one class per line. It is the same for every finding of a probe, whatever the verdict, and it is an empty string when there is no text for the probe. Files written before this field existed do not have it.
remediation_actions splits the same advice into steps by how fast you can ship them. Each entry has tier, text and atlas_mitigation. tier is mitigation (a configuration change you can make today, such as an approval gate, a filter or an allowlist) or fix (the design change that removes the weakness). atlas_mitigation is a MITRE ATLAS mitigation id such as AML.M0030, used as a tag, or null. Every built-in probe has at least one entry of each tier: one of each for a single attack class or a probe with its own text, and one of each per attack class otherwise, in the probe's class order. The list is the same for every finding of a probe. It is [] for scan-skill findings, for integration scenarios without attack_class, and for a probe-pack probe that sets its own remediation without remediation_actions. remediation keeps its old text, so tools that read only that field see no change. Files written before this field existed do not have it.
Finding events
events records steps backed by evidence that Argus observed or the target reported. Every new finding includes the key. Findings without a supported source of events contain [], including plain chat, MCP agent_world, browser, payment, and direct scan-skill findings. Files written before this field existed do not have it. The format remains argus.run/1.
Each event has these fields in this order:
| Field | Type | Meaning |
|---|---|---|
event_id |
string | Nonempty identifier, unique within this finding. |
parent_event_ids |
array of strings | IDs of earlier evidence-backed steps in the same finding. Empty when no parent identifier resolves. |
ts |
string or null | UTC wall-clock time with milliseconds and a trailing Z, such as 2026-11-01T04:10:24.123Z. Null when the source has no timezone-aware wall-clock time. |
actor |
string | Nonempty role name, such as argus, target_agent, planner, reviewer, executor, entry, or worker:<peer>. |
operation |
string | One of the six values below. |
tool_name |
string or null | Tool or action name when recorded. Corresponds to OTel gen_ai.tool.name. |
tool_call_id |
string or null | Tool-call identifier when recorded. Corresponds to OTel gen_ai.tool.call.id. |
object_ref |
string or null | <kind>:<id>, with a kind from the table below and a nonempty ID without whitespace. Null when no supported object identifier exists. |
basis |
string | observed when Argus, the testbed, or a third-party API observed the step. reported when the target supplied it in telemetry or a run log. |
atlas_technique |
string or null | ATLAS technique ID such as AML.T0051 or AML.T0051.001. Both current adapters emit null. Finding-level mappings remain in atlas_id. |
source |
string | a2a_world or integration_scenario. |
summary |
string or null | At most 200 characters, assembled only from actor, operation, tool_name, and the object kind. Contains no object ID or payload. |
seq |
integer or null | source record order within a finding; does not guarantee event occurrence order; nullable. |
Non-null seq values are nonnegative and unique within a finding. They support layout ordering and are not comparable across findings. When both a parent and its child have seq, the parent's value is smaller. A2A uses the testbed row's seq, with a trial-index offset of 1,000,000 per trial when combining trials. Integration scenarios record planted objects in registry order, then retain each evidence row's position after those objects. Deduplicated events keep their first recorded position.
The operation set is closed:
| Operation | Meaning | Current source |
|---|---|---|
invoke_agent |
An actor hands a task or message to an agent. | The scenario trigger, A2A agent_message, or A2A peer_message when peer_trusted is not false. |
execute_tool |
An agent calls a tool or action interface. | An A2A action sink row. tool_name contains the recorded action name. |
deliver_message |
A message enters a channel that an agent can read. | An A2A peer_reply or a scenario seed email sent by Argus. |
read_object |
An actor reads an object. | A successful target run-log read, retrieve, search, get, or fetch operation that is not a dry run. |
write_object |
An actor creates or changes an object. | An Argus-planted document or a Gmail draft_created or draft_changed observation. |
send_external |
Data goes to a destination outside the target's trust boundary, with an observed boundary marker. | An A2A peer_message with peer_trusted false. |
invoke_agent and execute_tool reuse OTel names. Argus adds no OpenTelemetry dependency. Writing a draft is write_object, even if its recipient is unexpected. A draft is not an observed send.
The object-kind set is closed:
| Kind | Identifier |
|---|---|
a2a.message |
A2A message ID. |
a2a.task |
A2A task ID, preferred for a peer reply when available. |
acs.email |
ID returned for a seed email sent through ACS. |
gdrive.file |
Google Drive file ID. |
gmail.message |
Gmail message ID, including the message inside a draft. |
gmail.draft is not a kind because the Gmail observer records the draft's message ID, which differs from its draft ID. mcp.tool has no emitter in this version. An A2A action has no object reference because its business references are canaries, not object IDs.
Downstream consumers build edges from parent_event_ids. Argus defines no separate edge or node types. Every parent ID resolves within the finding, and the graph has no cycles. An invalid adapter graph produces a warning and events: []. The two supported bases are observed and reported. There is no inferred basis, and judge reasoning never creates an event or a parent edge.
Only A2A world attack phases and the mail_policy_conflict_v1 integration scenario currently emit events. A2A event IDs have a t<trial>: prefix. Invalid trial evidence emits no events. Discovery reads (bootstrap_read, card_read, and task_poll) and clean-phase rows are excluded. A2A parents resolve only recorded parent_event_ids and parent_message_ids against earlier emitted rows' event, message, or task IDs. Unresolved and forward references are dropped. Shared business values alone never create a parent edge.
The mail scenario uses test-group evidence plus the planted objects that evidence references, including a baseline policy document when referenced. Only a runner-created response row with TARGET_SELF_REPORT provenance becomes the observed Argus trigger. That trigger links to objects whose markers or IDs Argus put in the prompt. A target run-log or third-party row named response does not become an Argus invocation. Successful run-log reads remain reported and link to the recorded call and the object identified by marker or ID. Target identity claims do not become observations through these links. A draft links to the seed object's marker, not to the trigger call. No recorded identifier ties the draft to that call. Repeated observations with the same draft operation and message ID produce one event. Control and baseline evidence rows, infrastructure folders, baseline_poll, draft_snapshot, object_changed, and failed, non-read, or dry-run run-log rows produce no events.
All A2A ts values are null because the testbed's monotonic offsets have no wall-clock anchor. Mail triggers use Argus's timezone-aware response timestamp. Planted objects have no timestamp, and mail draft and run-log observations have no occurrence time, so their ts values are null. Poll time is not occurrence time. Naive datetimes also become null. Parent links express the recorded relationships. Neither seq nor array position guarantees event occurrence order.
Events exclude prompts, replies, raw tool arguments and outputs, email bodies, field values, and A2A references or canaries. Those payloads remain only in evidence, under its existing redaction rules. Event text is masked as described below. SARIF does not include events or codeFlows.
evidence is whatever the target adapter observed, such as tool calls, or null for text-only adapters. Its contents are free-form. Values that are not plain JSON are written as strings: timestamps in ISO 8601, UUIDs in their usual form, bytes decoded as UTF-8 (invalid bytes become U+FFFD), and any other type through str().
For a browser_use target, evidence is the record of what the agent did on the trap page. It has three keys. page_loads counts how often the agent opened the trap page. trap_requests lists every request the agent sent to the trap's sink, in arrival order. Each entry has method, path, query, body and complete. path leaves out the trap's random path token, so it reads /sink/... on every run. The sink keeps at most 50 requests and 4000 characters of each body. complete is true when body holds the whole body. It is false when the body was longer than 4000 characters, when it stalled for 5 seconds, or when it was still arriving 2 seconds after the agent returned. The assertion also detects a whole canary spelled by successive body-less GET or HEAD requests in arrival order. Each piece is either the sole nonempty query value on the same path and query key, or the final path segment under the same prefix when there is no query. Raw pieces and every intermediate URL-decoded view up to three rounds remain eligible, so literal plus signs and percent sequences survive further decoding. Pieces must concatenate to the whole canary at request boundaries; ambiguous queries, other routes and other request types break the sequence. For canaries with spaces or dashes and at least eight characters after removing those separators, spaces and dashes in the pieces may also be omitted. Failures name the requests involved. If any entry is incomplete and neither an entry nor a sequence contains the canary, no_canary_in_trap_requests cannot decide, and the finding is an error. Queries and bodies can hold real credentials, for example ones the browser autofilled, so the evidence masks them. Each form field value (in a query or a form body) and each JSON string or number that does not contain one of the probe's canaries becomes <redacted N chars>, where N is the length of the decoded value. A body or query that is neither form fields nor JSON keeps its text only if it contains a canary; otherwise it becomes <redacted N chars> as a whole. The sole query value in each request involved in a detected canary sequence is also kept, so masking preserves the sequence verdict; unrelated values remain masked. Field names, JSON keys, method, path and complete are kept. A probe without a no_canary_in_trap_requests assertion has no canary, so every value is masked. The assertions read the masked record and reach the same verdict they would on the raw one. interaction_count is page_loads plus the number of sink requests. For a browser probe with assertions, no sink request and no page load means the agent never touched the trap, and the finding is an error, not a pass. Every bundled browser probe with assertions works this way. The rule does not apply to a probe without assertions, such as the 6 shared probes that also run against chat targets (counted on 2026-10-30): the judge decides those from the reply, whether or not the agent opened the page, except for the marker check in the next paragraph. It also does not apply to a probe that sets requires_interaction: false.
A probe whose attack text sits behind a URL the agent must fetch lists contact_markers in its YAML. These are words that appear only in the fetched content and never in what Argus sends. When the judge returns pass and the reply contains none of those words, nothing shows the agent read the content. The finding is then skipped, not pass, with evidence.exposure set to not_exercised. A fail or warn is kept as the judge gave it. When the judge decided it (every assertion the probe declares passed, or it declares none), its reply has none of the words, and the trap recorded no sink request, its evidence.exposure is also set to not_exercised, because nothing shows the payload reached the agent. It is not set when the trap record could not be read. An agent that did fetch the page but named nothing from it is also reported skipped. ambient_persuasion_url_fetch and the ten browser_dom_inject_* probes carry contact_markers (counted on 2026-11-12). A probe can set no_contact_reason to print its own reason for the skip; the DOM-injection probes name the placement the agent did not read.
Trial evidence
Static probes with trials above 1 produce one finding per whole probe. Its evidence contains trial_summary and trials, matching the outer format used by paired probes. At 1, static output keeps its existing findings and evidence without these additions. YAML trials and the run-level override accept only integers from 1 to 20; the override replaces the probe value.
trial_summary contains attempted, pass, fail, warn, error, skipped, blocked_by_target, valid, verdict and reason_code. valid is pass + fail + warn. These counts describe whole-probe trials, not prompts. One failing prompt makes a trial fail, and any failing trial makes the final verdict fail. Mixed pass and error outcomes cannot become pass. A cost-interrupted trial with no established fail or warn has verdict aborted_cost; that outcome counts as attempted but not valid or in the seven outcome counters.
Each entry in trials has trial, verdict, reason_code, reason, clean and attack. Static entries have clean: null and attack.observations, a list of finalized prompt results. Each observation contains attack_prompt, target_response, verdict, reasoning, target_latency_ms and evidence. These are independent JSON snapshots without nested finding IDs or timestamps. Paired entries retain the clean and attack phase evidence. A paired trial whose attack never reached the agent is skipped with reason_code attack_not_delivered and an extra exposure: not_exercised.
A failing aggregate explains the number of failed and valid trials, such as 2 of 4 valid trials failed. All requested trials must complete and pass before the report uses the weak-evidence all-pass sentence. If a cost or transport budget interrupts execution, the finding keeps only completed and started trial rows and the run is incomplete. Transport cleanup remains in target_cleanup; each send retains its existing attempt cleanup even when multiple sends belong to one trial.
Redaction
People commit reports to CI artifacts and upload SARIF to GitHub code scanning, so in local mode (--local) Argus masks secrets before it writes any report. Every format (json, html, sarif and junit) goes through the same masking step, so a secret masked in one format is masked in all of them. Reports from a server implementing the /api/v1/redteam API are rendered by that server and are not covered here. A masked value is the string ***. Three rules apply.
Rule 1: substituted environment values
A target spec can reference environment variables as $VAR or ${VAR} in any string (and %VAR% on Windows). Argus records every non-empty value it substitutes. Before writing, it replaces each of those values with *** in every string in the report. That covers the target spec, target_response, reasoning and evidence at any depth, including dict keys inside evidence. If two evidence keys mask to the same text, both are kept, the second as ***#2. Longer values are replaced first, so a value that contains another one is masked whole.
This rule does not know which variables are secret. A non-secret value such as a port number or a hostname is masked too, wherever its characters appear in free text. This over-redaction is deliberate. These fields are always written as the engine produced them, never masked:
- run:
schema_version,argus_version,id,status,started_at,finished_at,coverage,summary, the deep replay metadata block andtarget_spec.kind - each finding:
id,probe_id,verdict,severity,probed_at,created_at,confidence,target_latency_ms,atlas_id,owasp_id,owasp_agentic,nist_id,eu_ai_act_id,judge_model,escalated_model,remediationandremediation_actions - each
eventsentry:event_id,parent_event_ids,ts,operation,basis,source,atlas_techniqueandseq
Only these event structure fields are restored after masking. actor, tool_name, tool_call_id, object_ref, and summary undergo normal text masking. If masking makes an object kind unknown, object_ref becomes null. summary is clipped to 200 characters after masking.
Rule 2: secret-looking key names
A secret written directly in the target file, without a ${VAR} reference, is caught by key name. In target_spec, at any depth, each key is compared after lowercasing it and removing _ and -. So api_key, apiKey and API-KEY are treated the same.
- A key ending in
key,tokenorauthheader, or containingsecret,password,authorization,credentialorcookie, has its whole value replaced with***. This catchesprivateKey,accessKey,access_token,clientSecretandauthHeader. Counters such asmax_tokensend intokens, do not match, and are kept. - A key named
headers, or ending inheaders(for exampleextra_headers), keeps every header name and masks every header value. This works for header lists such as[["Authorization", "Bearer ..."]]and for header objects such as{"Authorization": "Bearer ..."}.
Rule 3: literal values found by rule 2
Every value that rule 2 masks is also added to the list from rule 1. So if the target echoes a literal API key or header value back in target_response or evidence, it is masked there too. For a header value with a scheme, such as Bearer abc123, both the full value and the part after the scheme (abc123) are masked.
Limits
Masking matches exact text. A secret that the target changes before echoing it, for example base64-encoded, URL-encoded, split across lines or reversed, is not caught. A secret written literally under a key name that rule 2 does not match, and with no ${VAR} reference, is not caught either. Pass secrets through environment variables so rule 1 covers them.
Compatibility
schema_version is "argus.run/1". Within version 1 the format only grows by adding optional fields. Removing or renaming a field changes the version to "argus.run/2". Tools that read run.json should ignore keys they do not know and check schema_version before reading anything else.
Paired runs and trials
Paired probes store each clean and attack world's sealed evidence in evidence.trials. Each entry includes trial, verdict, reason_code, reason, clean, and attack. An attack is null when the clean control failed.
evidence.trial_summary contains attempted, pass, fail, warn, error, skipped, blocked_by_target, valid, verdict, and reason_code. The valid denominator is pass plus fail plus warn. The finding's reasoning repeats all seven counts and the valid denominator so each report format retains them.
A failure takes precedence over warning. Only all-pass trials produce pass. Mixed incomplete trials produce error. A cost cap retains an observed fail or warn, otherwise the result is aborted_cost. A pass records only that the behavior was not observed in this environment on that date.
MCP world snapshots may also carry resource_reads for resource worlds, tool_calls[].server: "second" for second-endpoint calls, and elicitations for form worlds. These fields are additive.
A2A paired-world evidence
a2a_agent retains the frozen Finding and run.json field contracts. Per-trial evidence lives in evidence.trials, with clean and attack snapshots, verdict and reason_code; aggregate counts remain in evidence.trial_summary.
Each snapshot uses schema_version: argus-a2a-evidence-v1, with world, phase, session_id, sealed, seal_timeout, capabilities, deployment, fresh canaries, trusted-peer registry, value claims, approvals_issued, events, passive actions, optional ingress, attack_delivered, rejected_requests, counters, budget and task_completed. Events retain sequence, identity assurance, matching digest, current references, parent references, transport/version and response state. Actions separately record request_received, accepted, applied, verified caller, operation and business fields.
Agent identities are verified by distinct configured credentials. Matching sent/received digests establish an observed edge; caller-supplied adoption flags do not. Current-world attribution requires fresh business references, not a reused message or parent ID. Unassociated authenticated actions cause an evidence error. Single-agent budget symptoms can warn; observed cross-agent violations fail, even when a later hard budget truncates observation. Missing evidence or clean capability proof cannot become a pass.
The controller masks full and bare auth credentials from business text and fields in live and sealed snapshots. Public structural metadata, such as role, schema/version, recognized capability, claim/delivery and transport enums, remains unchanged when a credential happens to equal a public label. Message IDs, parent IDs and arbitrary business strings inside these evidence snapshots are never restored. Normalized finding event IDs follow the separate restoration rule above. CLI report redaction applies again to target credentials and receiver headers.
In hosted mode --probes all leaves out the browser_url_sequence_exfil probe; named, it reports skipped because redacted query values and per-request marker hits cannot reconstruct URL pieces; the CLI prints that reason.