From 64e44341ee4c62b8edb09a950cc275a6bd1d796d Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:18:30 +0100 Subject: [PATCH] feat(detection-core): bound model execution handoff --- .kiro/specs/detection-core/tasks.md | 2 +- src/openultrasast/hunt/model_contract.py | 145 +++++++++++++++++++++++ tests/test_hunt_model_contract.py | 117 ++++++++++++++++++ 3 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 src/openultrasast/hunt/model_contract.py create mode 100644 tests/test_hunt_model_contract.py diff --git a/.kiro/specs/detection-core/tasks.md b/.kiro/specs/detection-core/tasks.md index 54012ae..a2ccf4c 100644 --- a/.kiro/specs/detection-core/tasks.md +++ b/.kiro/specs/detection-core/tasks.md @@ -310,7 +310,7 @@ - _Requirements: 3.5, 14.3_ - _Boundary: Authorization Adapter_ - _Depends: 3.3, 7.2_ -- [ ] 9.2 (P) Translate hunt-owned model execution through the bounded handoff +- [x] 9.2 (P) Translate hunt-owned model execution through the bounded handoff - Convert structured executor inputs and outputs to the canonical request/result contracts. - Keep prompt creation, provider calls, caching, parsing, and redaction in the hunt subsystem. - Reject mismatched declared results through core validation without importing DSPy into core. diff --git a/src/openultrasast/hunt/model_contract.py b/src/openultrasast/hunt/model_contract.py new file mode 100644 index 0000000..96bb704 --- /dev/null +++ b/src/openultrasast/hunt/model_contract.py @@ -0,0 +1,145 @@ +"""Translate injected hunt executor I/O into reproducible core model records. + +Prompt rendering, provider execution, caching, parsing, storage, and redaction stay +outside this module. The adapter only freezes the structured handoff and asks the +dependency-free core to validate the returned identities and outcome. +""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from openultrasast.core import canonical, model +from openultrasast.core.evidence import EvidenceRecord +from openultrasast.core.family import FamilyContract, ResidualQuestion +from openultrasast.core.provenance import ModelCallIdentity +from openultrasast.core.semantics import FieldValue + + +@dataclass(frozen=True) +class ExecutorModelInput: + """Frozen structured input from which hunt-owned code may render a prompt.""" + + request_digest: str + packet_digest: str + family_contract_digest: str + residual_question_id: str + completeness_digest: str + program_declaration_digest: str + record: EvidenceRecord + contract: FamilyContract + residual_question: ResidualQuestion + completeness: tuple[model.CompletenessItem, ...] + + +@dataclass(frozen=True) +class ExecutorModelOutput: + """Untrusted structured output supplied by the hunt-owned executor.""" + + request_digest: str + packet_digest: str + provider: str + model: str + program_digest: str + prompt_digest: str + configuration_digest: str + adapter_revision: str + status: str + normalized_fields: tuple[FieldValue, ...] + inline_redacted_text: str | None + artifact: canonical.ContentRef | None + redaction_policy_digest: str | None + family_decision: str | None + decision_mapping_digest: str | None + failure_reason: str | None + + +ModelExecutor = Callable[[ExecutorModelInput], ExecutorModelOutput] + + +def model_executor_input(request: model.ModelRequest) -> ExecutorModelInput: + """Validate and freeze a core request for hunt-owned prompt/execution code.""" + model.model_request_bytes(request) + return ExecutorModelInput( + request.request_id, + request.packet_digest, + request.family_contract_digest, + request.residual_question_id, + request.completeness_digest, + request.program.declaration_digest, + request.record, + request.contract, + request.residual_question, + request.completeness, + ) + + +def _response(output: ExecutorModelOutput) -> model.PreservedModelResponse | None: + absent = ( + not output.normalized_fields + and output.inline_redacted_text is None + and output.artifact is None + and output.redaction_policy_digest is None + ) + if absent: + return None + if output.redaction_policy_digest is None: + raise model.ModelContractError("preserved executor response requires redaction_policy_digest") + return model.make_preserved_model_response( + normalized_fields=output.normalized_fields, + inline_redacted_text=output.inline_redacted_text, + artifact=output.artifact, + redaction_policy_digest=output.redaction_policy_digest, + ) + + +def _status(value: str) -> model.ModelStatus: + try: + return model.ModelStatus(value) + except (TypeError, ValueError) as exc: + raise model.ModelContractError("executor status must be succeeded or failed") from exc + + +def _result(output: ExecutorModelOutput) -> model.ModelResult: + if type(output) is not ExecutorModelOutput: + raise model.ModelContractError("executor must return an exact ExecutorModelOutput") + return model.ModelResult( + output.request_digest, + output.packet_digest, + ModelCallIdentity( + output.provider, + output.model, + output.program_digest, + output.prompt_digest, + output.configuration_digest, + output.adapter_revision, + ), + _status(output.status), + _response(output), + output.family_decision, + output.decision_mapping_digest, + output.failure_reason, + "", + ) + + +def execute_model_contract( + request: model.ModelRequest, + executor: ModelExecutor, + resolver: canonical.ContentResolver, +) -> model.ModelResult: + """Execute once through an injected hunt boundary and validate the exact result.""" + frozen = model_executor_input(request) + result = _result(executor(frozen)) + model.validate_model_result(request, result, resolver) + return result + + +__all__ = [ + "ExecutorModelInput", + "ExecutorModelOutput", + "ModelExecutor", + "execute_model_contract", + "model_executor_input", +] diff --git a/tests/test_hunt_model_contract.py b/tests/test_hunt_model_contract.py new file mode 100644 index 0000000..5aabf74 --- /dev/null +++ b/tests/test_hunt_model_contract.py @@ -0,0 +1,117 @@ +"""Hunt-owned execution crosses the dependency-free core model boundary.""" + +from __future__ import annotations + +import ast +from dataclasses import replace +from pathlib import Path + +import pytest +from test_core_model import _case, _digest + +from openultrasast.core import model +from openultrasast.hunt.model_contract import ( + ExecutorModelOutput, + execute_model_contract, + model_executor_input, +) + + +def _output(result: model.ModelResult) -> ExecutorModelOutput: + identity = result.identity + return ExecutorModelOutput( + request_digest=result.request_digest, + packet_digest=result.packet_digest, + provider=identity.provider, + model=identity.model, + program_digest=identity.program_digest, + prompt_digest=identity.prompt_digest, + configuration_digest=identity.configuration_digest, + adapter_revision=identity.adapter_revision, + status=result.status.value, + normalized_fields=() if result.response is None else result.response.normalized_fields, + inline_redacted_text=None if result.response is None else result.response.inline_redacted_text, + artifact=None if result.response is None else result.response.artifact, + redaction_policy_digest=None if result.response is None else result.response.redaction_policy_digest, + family_decision=result.family_decision, + decision_mapping_digest=result.decision_mapping_digest, + failure_reason=result.failure_reason, + ) + + +@pytest.mark.parametrize("failed", [False, True]) +def test_fake_executor_success_and_failure_cross_canonical_boundary(failed: bool) -> None: + case = _case(failed=failed) + seen = [] + + def executor(value): + seen.append(value) + return _output(case.result) + + result = execute_model_contract(case.request, executor, case.resolver) + + assert seen == [model_executor_input(case.request)] + assert result == case.result + model.validate_model_result(case.request, result, case.resolver) + + +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ("request_digest", _digest("wrong-request"), "request"), + ("packet_digest", _digest("wrong-packet"), "packet"), + ("program_digest", _digest("wrong-program"), "program"), + ("decision_mapping_digest", _digest("wrong-mapping"), "mapping"), + ], +) +def test_executor_cannot_substitute_declared_result_identity(field: str, value: str, message: str) -> None: + case = _case() + output = replace(_output(case.result), **{field: value}) + + with pytest.raises(model.ModelContractError, match=message): + execute_model_contract(case.request, lambda _: output, case.resolver) + + +def test_failure_shape_and_success_decision_are_core_validated() -> None: + case = _case() + failure = replace( + _output(case.result), + status="failed", + family_decision=None, + decision_mapping_digest=None, + failure_reason=None, + ) + with pytest.raises(model.ModelContractError, match="failure_reason"): + execute_model_contract(case.request, lambda _: failure, case.resolver) + + forbidden = replace(_output(case.result), family_decision="not-declared") + with pytest.raises(model.ModelContractError, match="allowed"): + execute_model_contract(case.request, lambda _: forbidden, case.resolver) + + +def test_hunt_adapter_owns_no_provider_or_dspy_execution() -> None: + import openultrasast.hunt.model_contract as adapter + + with open(adapter.__file__, encoding="utf-8") as source_file: + source = source_file.read() + assert "import dspy" not in source + assert "litellm" not in source + assert "openai" not in source + + +def test_core_import_census_has_no_model_runtime_dependency() -> None: + core = Path(__file__).parents[1] / "src" / "openultrasast" / "core" + forbidden = {"dspy", "litellm", "openai"} + found = [] + for path in sorted(core.glob("*.py")): + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + for node in ast.walk(tree): + if isinstance(node, ast.Import): + names = {alias.name.split(".", 1)[0] for alias in node.names} + elif isinstance(node, ast.ImportFrom) and node.module: + names = {node.module.split(".", 1)[0]} + else: + continue + if names & forbidden: + found.append((path.name, node.lineno, tuple(sorted(names & forbidden)))) + assert found == []