Skip to content

Result schema

What the async queue (enqueue + consume_next_event) and the blocking scan_* helpers return. Python names are shown; the Rust types (SecurityScanResult, LayerResult, EvaluationResult, QueuedSecurityEvent, SecurityFailure) are in the Rust API reference.

Scan result

A single result — the result field of a queue result event, or one element of the list the blocking scan_* helpers return — is a dictionary describing one category's verdict:

[
    {
        "category": "dlp",
        "class_name": "safe",
        "confidence": 1.0,
        "level": "L1",
        "model": "native:dlp",
        "duration_ms": 0.4,
        "evidence_spans": [],
        "label_scores": [],
        "layers": [
            {
                "level": "L1",
                "layer_type": "native",
                "class_name": "safe",
                "confidence": 1.0,
                "matched": True,
                "duration_ms": 0.4,
                "thresholds": {},
                "details": {},
            }
        ],
    }
]
Field Type Meaning
category str The scan category this result belongs to.
class_name str Winning class (a category-specific label, or safe).
confidence float Confidence in class_name, 0.01.0.
level str The level that produced the winning verdict: L1, L2, or L3.
model str The producing scanner, e.g. native:dlp, external:<id>, or a model id.
duration_ms float Wall-clock time spent producing this result.
decision dict Structured classifier decision envelope for terminal model-backed classifier results. Python omits this key when no authoritative classifier decision exists; Rust exposes SecurityScanResult.decision: Option<DecisionEnvelope>.
evidence_spans list Exact matched spans (PII/DLP/dynamic-pii); empty for safe/model-only results.
label_scores list Per-label scores for multi-label heads (e.g. tool_tags); each entry is {label, confidence, matched}. Empty for single-label results.
layers list Per-layer breakdown of everything that ran for this category.

Layer entry

Each element of layers records one layer's output:

Field Type Meaning
level str L1 / L2 / L3.
layer_type str e.g. native, or the model layer type.
class_name str This layer's class.
confidence float This layer's confidence.
matched bool Whether this layer produced a positive match.
duration_ms float Wall-clock time spent in this layer.
thresholds dict Thresholds applied at this layer (operating point, etc.).
details dict Layer-specific extra detail. NTDB L2 layers add L3 promotion context; L3 layers add chunk execution metadata. The stable policy contract is decision, not free-form details.

Decision envelope

Model-backed classifier pipelines (injection, threat, routing, sensitive_document, tool_class, tool_action, and tool_tags) apply the configured final-decision threshold profile after raw L2/L3 scoring. The top-level class_name / confidence is the accepted final verdict. When no candidate passes its threshold, the result falls back to the pipeline default class (benign, unknown, other, etc.) and uses the model's default-class confidence when available, or 0.0 otherwise.

Terminal classifier results expose that policy input and Ark's calibrated recommendation under decision. decision.is_some() is the lifecycle marker for an authoritative classifier policy input. Early L2 results that still contain an l3_pending layer, provisional events, progress events, and result-preview events leave decision unset (None in Rust) even when they carry classifier-looking scores.

{
    "schema_version": "ark.decision.v1",
    "final_result": {"class_name": "benign", "confidence": 0.0, "source": "default"},
    "decision_candidate": {
        "source": "l2",
        "class_name": "instruction_override",
        "confidence": 0.5640919208526611,
        "acceptance_threshold": 0.86471,
        "accepted": False,
        "evidence": None,
    },
    "recommendation": {
        "accepted": False,
        "final_arbitration": "default",
        "operating_point": "best_f1",
        "acceptance_threshold": 0.86471,
    },
    "candidates": [
        {
            "source": "l2",
            "class_name": "instruction_override",
            "confidence": 0.5640919208526611,
            "acceptance_threshold": 0.86471,
            "accepted": False,
            "evidence": None,
        }
    ],
    "terminality": {"completion": "complete", "degraded": False, "degradation_reason": None},
    "provenance": {
        "ark_version": "0.1.3",
        "schema_version": "ark.decision.v1",
        "model": "unified-v3-threat",
    },
}
Field Type Meaning
schema_version str Decision-envelope schema version. Currently ark.decision.v1.
final_result dict The final Ark verdict after threshold arbitration: {class_name, confidence, source}.
decision_candidate dict | null Canonical policy input. It is the winning accepted candidate when final_arbitration is l2, l3, or union; for default, it is the highest-priority rejected candidate in l3, union, l2 order. None when no valid classifier candidate exists.
recommendation dict Ark's calibrated default recommendation. accepted is false only for final_arbitration: "default".
candidates list All typed L2, L3, and Union candidates available to arbitration.
terminality dict Completion state for the result: completion, degraded, and optional degradation_reason.
provenance dict Minimal source provenance: ark_version, schema_version, and model.

Candidate entries have this shape:

Field Type Meaning
source str l2, l3, or union. final_result.source may also be default.
class_name str Candidate class before downstream Patronus policy.
confidence float Candidate confidence from that source.
acceptance_threshold float Ark's calibrated acceptance threshold for this candidate's source/class/operating point.
accepted bool Whether this candidate passed Ark's calibrated acceptance threshold.
evidence dict | null Extra candidate evidence. Union candidates include l2_weight, l3_weight, l2_confidence, and l3_confidence.

Example: if Threat L2 predicts instruction_override at 0.5640919, but the selected threshold profile requires a higher L2 confidence, the top-level result is the default benign class while decision.decision_candidate retains the rejected l2 candidate with acceptance_threshold: 0.86471 and decision.recommendation.accepted: False.

label_scores[].matched is layer-local model output metadata. It is not threshold acceptance and must not be treated as equivalent to decision.recommendation.accepted or decision.candidates[].accepted.

Candidate confidences are calibrated for Ark's bundled threshold profiles. Treat them in the context of their category, source, model, and operating_point; do not compare scores across unrelated sources or model versions without their matching thresholds.

Evidence spans

Native PII and DLP findings, and dynamic-pii entities, populate evidence_spans with exact offsets:

for span in result["evidence_spans"]:
    print(span["label"], span["text"], span["score"], span["start_byte"], span["end_byte"])

Each span carries the matched label, the matched text, a score, and both byte and character offsets (start_byte/end_byte/start_char/end_char). Safe native results leave evidence_spans empty.

Async queue events

consume_next_event(timeout) returns one event dict at a time (or None on timeout). consume_events(timeout) yields them. There are up to four event_types: result and finished always, plus progress and provisional when L3 progress reporting is enabled (execution_gates.l3.progress, off by default). The blocking scan_* helpers only ever return result / finished.

result

{
    "event_type": "result",
    "request_id": "…",
    "result": { a scan result dict, as above },
}

One request can emit several result events. L1 results are visible as soon as L1 finishes; L2 and a later promoted L3 result follow independently.

Promoting NTDB L2 layers expose details.l3_candidates (each entry: byte span, promote_score, promote_threshold, source_pipeline, source_model, l2_class) and details.l2_chunk_outputs (the per-chunk L2 outputs retained for aggregation, with the raw embeddings stripped). Under l3_strategy="multi", candidates from every promoted pipeline in the coalesced run are deduplicated by exact span match — when two heads promote the same span, source_pipeline / source_model / l2_class become comma-joined unions; under the default l3_strategy="dedicated" each pipeline's candidates stay separate and this merge never runs. The L3 worker maps candidate spans onto tokenizer-bounded windows; with representative clustering enabled it groups near-duplicate windows, infers only cluster representatives, and propagates their verdict to the rest (see L3 worker policy). An empty or unusable candidate list deliberately falls back to full-text L3.

progress

Opt-in, non-authoritative status while a long L3 scan resolves — only under the dedicated L3 strategy (l3_strategy="multi" never emits it):

{
    "event_type": "progress",
    "request_id": "…",
    "progress": {
        "category": "injection", "model": "…", "stage": "l3_chunk",
        "completed_chunks": 3, "total_chunks": 8,
        "inferred_chunks": 2, "propagated_chunks": 1, "cache_hits": 0,
        "early_exit": False, "coverage": 0.375, "details": {  },
    },
}

stage is one of l3_started, l3_chunk, l3_cluster_propagated, l3_early_exit. A progress event carries no verdict — never treat it as a result.

provisional

Same shape as a result event's result, but an interim preview re-aggregated from the chunks resolved so far. It is non-authoritative and may be superseded by later result / finished events; only emitted when execution_gates.l3.progress is set to "provisional".

finished

Exactly one terminal event follows all results for a request:

{
    "event_type": "finished",
    "request_id": "…",
    "completion": "…",     # "complete" | "degraded" | "failed"
    "failures": [  ],     # structured failures, if any
}

completion is one of complete (all planned work succeeded), degraded (some layer failed but a lower-layer result was delivered), or failed (no usable result). Consuming finished removes all library state for that request ID. Correlate every event by request_id.

Failures

Each failures entry is a structured SecurityFailure dict:

Field Type Meaning
stage str warmup, asset, scanner, inference, queue, or worker.
kind str not_ready, missing_asset, integrity_failure, initialization_failure, inference_failure, timeout, worker_unavailable, or internal.
level str | null The level that failed (L1/L2/L3), if applicable.
detector_id str | null The specific detector or model that failed, if applicable.
retryable bool Whether the failure is transient and could succeed on retry.
message str Human-readable description.

A failure does not throw during scanning — the scan degrades to the best available lower-layer result and reports the failure here. (warmup() itself is the exception: a missing required asset there raises rather than degrades.)

Request introspection

Method Returns
has_request(request_id) Whether the gateway still tracks this request.
request_state(request_id) Current state dict, or None.
is_finished(request_id) True / False, or None if unknown.
runtime_readiness() Initialized runtime state (levels ready, per stage/kind).

Request state reflects SecurityRequestState — whether any planned scanner or promoted L3 job can still publish an event.