360 lines
13 KiB
Python
360 lines
13 KiB
Python
"""
|
|
NeMo/Nemotron External Runner Preflight
|
|
======================================
|
|
|
|
Validates the local request pack before it is handed to an approved external
|
|
NeMo/NIM/Nemotron runner. This module does not call external services, tools,
|
|
production systems, or LLMs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from src.services.agent_nemotron_replay_adapter import (
|
|
NEMOTRON_CANDIDATE_ID,
|
|
REQUEST_SCHEMA_VERSION,
|
|
)
|
|
from src.services.agent_replay_input import assert_no_evaluation_label_leak
|
|
|
|
PREFLIGHT_SCHEMA_VERSION = "agent_nemotron_external_runner_preflight_v1"
|
|
|
|
_REQUIRED_RESPONSE_FIELDS = {
|
|
"proposed_action",
|
|
"action_plan",
|
|
"risk_level",
|
|
"requires_human_approval",
|
|
"blocked_by_policy",
|
|
}
|
|
_FORBIDDEN_TEXT_MARKERS = {
|
|
"evaluation_labels",
|
|
"verification_result",
|
|
"execution_success",
|
|
"execution_error",
|
|
"self_healing_score",
|
|
"rca_correct",
|
|
"tool_dry_run_pass",
|
|
"repair_success",
|
|
"false_repair",
|
|
}
|
|
_SENSITIVE_TEXT_MARKERS = {
|
|
"authorization",
|
|
"bearer ",
|
|
"basic ",
|
|
"password",
|
|
"passwd",
|
|
"api_key",
|
|
"secret",
|
|
"token",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class NemotronExternalRunnerPreflightReport:
|
|
"""Preflight decision for a NeMo external replay request pack."""
|
|
|
|
fixtures: int
|
|
candidate_inputs: int
|
|
requests: int
|
|
valid: bool
|
|
failures: list[str] = field(default_factory=list)
|
|
duplicate_fixtures: list[str] = field(default_factory=list)
|
|
duplicate_candidate_inputs: list[str] = field(default_factory=list)
|
|
duplicate_requests: list[str] = field(default_factory=list)
|
|
missing_candidate_inputs: list[str] = field(default_factory=list)
|
|
missing_requests: list[str] = field(default_factory=list)
|
|
unexpected_candidate_inputs: list[str] = field(default_factory=list)
|
|
unexpected_requests: list[str] = field(default_factory=list)
|
|
candidate_input_label_leak_records: int = 0
|
|
request_context_label_leak_records: int = 0
|
|
request_only_records: int = 0
|
|
not_replacement_evidence_records: int = 0
|
|
expected_action_marker_records: int = 0
|
|
sensitive_marker_present_in_context: bool = False
|
|
sensitive_marker_records: int = 0
|
|
sensitive_marker_distribution: dict[str, int] = field(default_factory=dict)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"schema_version": PREFLIGHT_SCHEMA_VERSION,
|
|
"candidate_id": NEMOTRON_CANDIDATE_ID,
|
|
"fixtures": self.fixtures,
|
|
"candidate_inputs": self.candidate_inputs,
|
|
"requests": self.requests,
|
|
"valid": self.valid,
|
|
"failures": list(self.failures),
|
|
"duplicate_fixtures": list(self.duplicate_fixtures),
|
|
"duplicate_candidate_inputs": list(self.duplicate_candidate_inputs),
|
|
"duplicate_requests": list(self.duplicate_requests),
|
|
"missing_candidate_inputs": list(self.missing_candidate_inputs),
|
|
"missing_requests": list(self.missing_requests),
|
|
"unexpected_candidate_inputs": list(self.unexpected_candidate_inputs),
|
|
"unexpected_requests": list(self.unexpected_requests),
|
|
"candidate_input_label_leak_records": self.candidate_input_label_leak_records,
|
|
"request_context_label_leak_records": self.request_context_label_leak_records,
|
|
"request_only_records": self.request_only_records,
|
|
"not_replacement_evidence_records": self.not_replacement_evidence_records,
|
|
"expected_action_marker_records": self.expected_action_marker_records,
|
|
"sensitive_marker_present_in_context": self.sensitive_marker_present_in_context,
|
|
"sensitive_marker_records": self.sensitive_marker_records,
|
|
"sensitive_marker_distribution": dict(self.sensitive_marker_distribution),
|
|
}
|
|
|
|
|
|
def evaluate_nemotron_external_runner_preflight(
|
|
*,
|
|
fixtures: list[dict[str, Any]],
|
|
candidate_inputs: list[dict[str, Any]],
|
|
requests: list[dict[str, Any]],
|
|
) -> NemotronExternalRunnerPreflightReport:
|
|
"""Validate request-pack readiness before an external NeMo runner consumes it."""
|
|
failures: list[str] = []
|
|
fixture_index, duplicate_fixtures = _index_records(fixtures, "fixture", failures)
|
|
input_index, duplicate_inputs = _index_records(
|
|
candidate_inputs,
|
|
"candidate_input",
|
|
failures,
|
|
)
|
|
request_index, duplicate_requests = _index_records(requests, "request", failures)
|
|
|
|
fixture_keys = set(fixture_index)
|
|
input_keys = set(input_index)
|
|
request_keys = set(request_index)
|
|
|
|
missing_inputs = sorted(_render_key(key) for key in fixture_keys - input_keys)
|
|
unexpected_inputs = sorted(_render_key(key) for key in input_keys - fixture_keys)
|
|
missing_requests = sorted(_render_key(key) for key in input_keys - request_keys)
|
|
unexpected_requests = sorted(_render_key(key) for key in request_keys - input_keys)
|
|
|
|
if missing_inputs:
|
|
failures.append(f"missing_candidate_inputs:{','.join(missing_inputs)}")
|
|
if unexpected_inputs:
|
|
failures.append(
|
|
f"unexpected_candidate_inputs:{','.join(unexpected_inputs)}"
|
|
)
|
|
if missing_requests:
|
|
failures.append(f"missing_requests:{','.join(missing_requests)}")
|
|
if unexpected_requests:
|
|
failures.append(f"unexpected_requests:{','.join(unexpected_requests)}")
|
|
|
|
candidate_input_label_leak_records = _candidate_input_label_leaks(
|
|
candidate_inputs,
|
|
failures,
|
|
)
|
|
request_context_label_leak_records = _request_context_label_leaks(
|
|
requests,
|
|
failures,
|
|
)
|
|
request_only_records = _count_request_metadata(requests, "request_only", True)
|
|
not_replacement_evidence_records = _count_request_metadata(
|
|
requests,
|
|
"not_replacement_evidence",
|
|
True,
|
|
)
|
|
expected_action_marker_records = sum(
|
|
1
|
|
for fixture in fixtures
|
|
if _expected_action_markers(fixture)
|
|
)
|
|
sensitive_marker_records, sensitive_marker_distribution = _sensitive_marker_scan(
|
|
candidate_inputs,
|
|
requests,
|
|
)
|
|
sensitive_marker_present = sensitive_marker_records > 0
|
|
if sensitive_marker_present:
|
|
failures.append(f"sensitive_marker_present_in_context:{sensitive_marker_records}")
|
|
|
|
_validate_requests(requests, failures)
|
|
_validate_context_alignment(
|
|
fixture_index=fixture_index,
|
|
input_index=input_index,
|
|
request_index=request_index,
|
|
failures=failures,
|
|
)
|
|
|
|
return NemotronExternalRunnerPreflightReport(
|
|
fixtures=len(fixtures),
|
|
candidate_inputs=len(candidate_inputs),
|
|
requests=len(requests),
|
|
valid=not failures,
|
|
failures=failures,
|
|
duplicate_fixtures=duplicate_fixtures,
|
|
duplicate_candidate_inputs=duplicate_inputs,
|
|
duplicate_requests=duplicate_requests,
|
|
missing_candidate_inputs=missing_inputs,
|
|
missing_requests=missing_requests,
|
|
unexpected_candidate_inputs=unexpected_inputs,
|
|
unexpected_requests=unexpected_requests,
|
|
candidate_input_label_leak_records=candidate_input_label_leak_records,
|
|
request_context_label_leak_records=request_context_label_leak_records,
|
|
request_only_records=request_only_records,
|
|
not_replacement_evidence_records=not_replacement_evidence_records,
|
|
expected_action_marker_records=expected_action_marker_records,
|
|
sensitive_marker_present_in_context=sensitive_marker_present,
|
|
sensitive_marker_records=sensitive_marker_records,
|
|
sensitive_marker_distribution=sensitive_marker_distribution,
|
|
)
|
|
|
|
|
|
def _index_records(
|
|
records: list[dict[str, Any]],
|
|
name: str,
|
|
failures: list[str],
|
|
) -> tuple[dict[tuple[str, str], dict[str, Any]], list[str]]:
|
|
indexed: dict[tuple[str, str], dict[str, Any]] = {}
|
|
duplicates: list[str] = []
|
|
for line_number, record in enumerate(records, start=1):
|
|
key = _run_incident_key(record)
|
|
if key is None:
|
|
failures.append(f"invalid_{name}:line_{line_number}:missing_run_or_incident")
|
|
continue
|
|
if key in indexed:
|
|
rendered = _render_key(key)
|
|
duplicates.append(rendered)
|
|
failures.append(f"duplicate_{name}:line_{line_number}:{rendered}")
|
|
continue
|
|
indexed[key] = record
|
|
return indexed, sorted(set(duplicates))
|
|
|
|
|
|
def _candidate_input_label_leaks(
|
|
candidate_inputs: list[dict[str, Any]],
|
|
failures: list[str],
|
|
) -> int:
|
|
leaks = 0
|
|
for line_number, candidate_input in enumerate(candidate_inputs, start=1):
|
|
try:
|
|
assert_no_evaluation_label_leak(candidate_input)
|
|
except Exception as exc:
|
|
leaks += 1
|
|
failures.append(f"candidate_input_label_leak:line_{line_number}:{exc}")
|
|
return leaks
|
|
|
|
|
|
def _request_context_label_leaks(
|
|
requests: list[dict[str, Any]],
|
|
failures: list[str],
|
|
) -> int:
|
|
leaks = 0
|
|
for line_number, request in enumerate(requests, start=1):
|
|
visible_payload = {
|
|
"incident_context": request.get("incident_context") or {},
|
|
"source_metadata": request.get("source_metadata") or {},
|
|
"user_prompt": request.get("user_prompt") or "",
|
|
}
|
|
markers = _forbidden_text_markers(visible_payload)
|
|
if markers:
|
|
leaks += 1
|
|
failures.append(
|
|
f"request_context_label_leak:line_{line_number}:"
|
|
f"{','.join(markers)}"
|
|
)
|
|
return leaks
|
|
|
|
|
|
def _validate_requests(
|
|
requests: list[dict[str, Any]],
|
|
failures: list[str],
|
|
) -> None:
|
|
for line_number, request in enumerate(requests, start=1):
|
|
if request.get("schema_version") != REQUEST_SCHEMA_VERSION:
|
|
failures.append(f"request_schema_mismatch:line_{line_number}")
|
|
if request.get("candidate_id") != NEMOTRON_CANDIDATE_ID:
|
|
failures.append(f"request_candidate_mismatch:line_{line_number}")
|
|
metadata = dict(request.get("metadata") or {})
|
|
if metadata.get("request_only") is not True:
|
|
failures.append(f"request_not_request_only:line_{line_number}")
|
|
if metadata.get("not_replacement_evidence") is not True:
|
|
failures.append(f"request_missing_not_replacement_evidence:line_{line_number}")
|
|
required = set((request.get("response_contract") or {}).get("required") or [])
|
|
missing_response_fields = sorted(_REQUIRED_RESPONSE_FIELDS - required)
|
|
if missing_response_fields:
|
|
failures.append(
|
|
"request_response_contract_missing:"
|
|
f"line_{line_number}:{','.join(missing_response_fields)}"
|
|
)
|
|
|
|
|
|
def _validate_context_alignment(
|
|
*,
|
|
fixture_index: dict[tuple[str, str], dict[str, Any]],
|
|
input_index: dict[tuple[str, str], dict[str, Any]],
|
|
request_index: dict[tuple[str, str], dict[str, Any]],
|
|
failures: list[str],
|
|
) -> None:
|
|
for key in sorted(set(fixture_index) & set(input_index)):
|
|
if fixture_index[key].get("incident_context") != input_index[key].get(
|
|
"incident_context"
|
|
):
|
|
failures.append(f"fixture_input_context_mismatch:{_render_key(key)}")
|
|
|
|
for key in sorted(set(input_index) & set(request_index)):
|
|
candidate_input = input_index[key]
|
|
request = request_index[key]
|
|
if candidate_input.get("incident_context") != request.get("incident_context"):
|
|
failures.append(f"input_request_context_mismatch:{_render_key(key)}")
|
|
if candidate_input.get("source_metadata") != request.get("source_metadata"):
|
|
failures.append(f"input_request_metadata_mismatch:{_render_key(key)}")
|
|
|
|
|
|
def _count_request_metadata(
|
|
requests: list[dict[str, Any]],
|
|
key: str,
|
|
expected: Any,
|
|
) -> int:
|
|
return sum(
|
|
1
|
|
for request in requests
|
|
if (request.get("metadata") or {}).get(key) is expected
|
|
)
|
|
|
|
|
|
def _expected_action_markers(fixture: dict[str, Any]) -> list[str]:
|
|
labels = dict(fixture.get("evaluation_labels") or {})
|
|
markers = labels.get("expected_action_markers") or []
|
|
return [str(marker) for marker in markers if str(marker).strip()]
|
|
|
|
|
|
def _sensitive_marker_scan(
|
|
candidate_inputs: list[dict[str, Any]],
|
|
requests: list[dict[str, Any]],
|
|
) -> tuple[int, dict[str, int]]:
|
|
distribution = dict.fromkeys(sorted(_SENSITIVE_TEXT_MARKERS), 0)
|
|
hit_records: set[tuple[str, str]] = set()
|
|
for record in [*candidate_inputs, *requests]:
|
|
key = _run_incident_key(record)
|
|
serialized = json.dumps(
|
|
record.get("incident_context") or {},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
).lower()
|
|
markers = [
|
|
marker for marker in sorted(_SENSITIVE_TEXT_MARKERS) if marker in serialized
|
|
]
|
|
if markers and key is not None:
|
|
hit_records.add(key)
|
|
for marker in markers:
|
|
distribution[marker] += 1
|
|
return len(hit_records), {key: value for key, value in distribution.items() if value}
|
|
|
|
|
|
def _forbidden_text_markers(payload: dict[str, Any]) -> list[str]:
|
|
serialized = json.dumps(payload, ensure_ascii=False, sort_keys=True).lower()
|
|
return sorted(
|
|
marker for marker in _FORBIDDEN_TEXT_MARKERS if marker in serialized
|
|
)
|
|
|
|
|
|
def _run_incident_key(record: dict[str, Any]) -> tuple[str, str] | None:
|
|
run_id = str(record.get("run_id", "")).strip()
|
|
incident_id = str(record.get("incident_id", "")).strip()
|
|
if not run_id or not incident_id:
|
|
return None
|
|
return (run_id, incident_id)
|
|
|
|
|
|
def _render_key(key: tuple[str, str]) -> str:
|
|
return f"{key[0]}::{key[1]}"
|