mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(detection-core): bound model execution handoff
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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 == []
|
||||
Reference in New Issue
Block a user