mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 17:58:18 +00:00
feat(detection-core): bound model execution handoff
This commit is contained in:
@@ -310,7 +310,7 @@
|
|||||||
- _Requirements: 3.5, 14.3_
|
- _Requirements: 3.5, 14.3_
|
||||||
- _Boundary: Authorization Adapter_
|
- _Boundary: Authorization Adapter_
|
||||||
- _Depends: 3.3, 7.2_
|
- _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.
|
- 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.
|
- 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.
|
- 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