Fact-Fidelity — engineering notes & recommendations¶
Companion to fact-fidelity.md. That doc explains what the
entailment check is; this one records the non-obvious design decisions, the
gotchas that cost real investigation, and the recommendations for
finishing the rollout. Written for whoever extends this next.
Design decisions (and why)¶
-
No separate
submit_findingtool. Every finding — deterministic emitter or LLM — already flows throughverify_finding, which validates into theFindingmodel (so the schema gate fires) and runs the entailment check. A second recording tool would be redundant and would widen the locked 45-tool product surface. The gateway already exists; we strengthened it. -
The verdict is already load-bearing — no
compute_verdictsurgery._apply_verifier_actions(scripts/find_evil_auto.py) drops arejectedfinding and downgrades adowngradedone beforecompute_verdictruns. So a misread that the verifier rejects can never reach a SUSPICIOUS verdict without any change to the verdict logic. We almost rewrote the verdict function before tracing this. -
INFERRED satisfies the gate via
derived_from, not a forced value. Many INFERRED findings are cross-fact inferences (DKOM =pslist0 ANDpsscanN>0) drawn from two different cited outputs — they have no single re-extractable value. Requiringasserted_valueson them would force a dishonest assertion or silently drop them. The gate requires values for CONFIRMED, and values orderived_fromfor INFERRED. -
entailment_okis a separate signal, not part ofoverall. It mirrorssignature_verified: an honest status that does not gate the presence-basedoverall, so dev/offline stub runs still verify end-to-end. Byte tampering of a sealed slice is already caught by the audit chain;entailment_okis the semantic re-check. -
Co-located
recordmatch. Binding several fields to one record stops a claim being assembled from a value in one row and a damning value in another — a gap the flat any-where match could not close.
Gotchas (each cost real time — read before extending)¶
-
_FINDING_MODEL_FIELDSsilently strips unknown fields.finding_for_verifierprojects a finding to just the typedFindingfields before the verifier sees it.asserted_valueshad to be added to that frozenset or it was dropped — the entailment check would have no-op'd forever and looked like it worked. If you add a new evidentiary Finding field, add it there too. -
event_id(and friends) are normalized — assert against the RAW serialized output, not the Python-side value. The orchestrator readsevent_idvia_event_id_value()because the input EVTX-XML nests it. But the Rustevtx_queryparser flattens it to a scalaru32before serialization, so the cited output'srows[*].event_idis a clean scalar. The asserted path must match the serialized tool output, which is what the verifier re-runs and hashes — not the bare value the Python iterates, and not the raw input. A path that doesn't resolve makes the finding silently fail entailment (drop), so verify the shape against the Rust tool / a fixture, never guess. -
The deterministic emitter path is a guarantee + emitter-bug catcher, not a misread catcher. The code that read the value re-asserts it, so it can't "misread" itself. The real teeth are on the LLM authoring path, where the model cannot record a fact the parser can't find. Frame claims accordingly.
-
_cffi_backendis missing in the dev env — Ed25519 crypto tests fail locally,StubSigner-path tests pass. Don't read those 9 failures as regressions; they pass in CI's pinned container. Manifest tests that useStubSigner(not Ed25519) do run locally — use that path to test custody logic without the backend. -
replayaudit records are hash-chain records but NOT Merkle leaves. Onlytool_call_output+finding_approvedbecome leaves. The sealed entailment slice rides on areplayrecord, so it is tamper-evident via the hash chain +audit_log_final_hash(whichverify_manifestchecks), not via the Merkle root. That's why the offline re-check adds value the Merkle root alone doesn't.
The honest scope boundary¶
Structured-value fidelity, not "hallucination solved." The check covers named values typed parsers emit (registry/EVTX/prefetch/MFT/USN/…). Interpretation ("these two artifacts mean lateral movement") has no deterministic oracle — it stays HYPOTHESIS, needs ≥2 artifact classes, and a human signs off. Pair every "solved the value-misread" with "did NOT solve inferential judgment." Never let this become "solved hallucination."
Recommendations (prioritized)¶
-
[HIGH] Flip the gate behind one live run — and consider fail-safe downgrade. Everything is wired + coverage-tested, but flipping
FIND_EVIL_REQUIRE_ASSERTED_VALUES=1by default changes verdict behavior on every run; a wrong path on real data would silently drop a real CONFIRMED finding → a falseNO_EVIL. Do the live run below first. Stronger still: change the gate from hard-reject to downgrade-with-audit-note on a missing declaration, so a missed emitter demotes a finding to a lead (surfaced) rather than dropping it — fail-safe instead of fail-closed. -
[MED] Persist the full matched output records for true offline re-extraction. Today
manifest_verifyre-checks the sealed matched values for consistency; it does not re-extract from the original full output (not persisted). Persisting the minimal matched records would let it re-run the real extractor offline, closing the gap between "consistency" and "re-extraction". -
[MED] Tighten the
recordmatch to per-field modes. It currently uses substring semantics for every field. Per-fieldexactvscontains(e.g.event_idexact,data_strcontains) removes the small loosening from the flat version. -
[LOW] Wire HYPOTHESIS emitters opportunistically. They are gate-exempt, but declaring values still seals their evidence and improves provenance.
-
[CONDITIONAL] If interactive/Claude-Code mode becomes the shipped product, add a typed
submit_findingas the sole recording path so verification is structurally non-skippable (today it's a guardrail, not a hard gate, on that path). -
[RESEARCH] Inference cross-check with a different model or rule engine. Pools A/B share a model (per the Estornell 2025 constraint), so they share blind spots on interpretation. An independent verifier lowers correlated error — probabilistically, never "solved." Keep it clearly tiered + human-owned.
How to flip the gate safely (the live run)¶
export FIND_EVIL_REQUIRE_ASSERTED_VALUES=1- Run
scripts/verdict <known-evil image with all artifact types>and a known-benign one. - Confirm the verdict and the CONFIRMED finding count are unchanged vs a baseline run with the flag off.
- Grep
audit.jsonlfor any finding rejected/downgraded for a missing or failedasserted_values— there should be none on the benign-shape paths. - Only then change the default.
Related¶
fact-fidelity.md— the mechanism.replay-determinism.md— the custody layer underneath.agent-config/SOUL.md,agent-config/TOOLS.md— the LLM contract this enforces.