Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/change/change_log.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This file provides a chronological, human-readable record of applied codebase an
*Note: For operational task and run history, consult `.triagecore/ledger.jsonl`.*

## [Unreleased]
- Implemented CR-036 (Model Manifest Runtime Warning Gate): Add a pure metadata-only route-to-manifest comparison report that warns on backend/model/route, alias-only identity, and incomplete integrity mismatches without blocking runtime execution.
- Implemented CR-035 (Backlog and Status Alignment Pass): Update current backlog and README status markers after CR-034, keeping runtime integrity enforcement and identity signing expansion out of scope.
- Implemented CR-034 (Repository Consistency and Secrets Hygiene): Align Python metadata with supported syntax, consolidate package metadata in `pyproject.toml`, expand CI's Python matrix, require environment-only Qwen API keys, reject secret-bearing persistent keys, sanitize backend HTTP errors, and add `SECURITY.md`.
- Implemented CR-033 (Model Manifest Check CLI): Add `tc model check --manifest <path>` to validate documented route-manifest fields without probing live backends or enforcing routing yet.
Expand Down
50 changes: 50 additions & 0 deletions docs/change/requests/CR-036-model-manifest-runtime-warning-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# CR-036: Model Manifest Runtime Warning Gate

## Status

Implemented

## Scope

Add a warning-only comparison layer between route metadata and a validated model
route manifest.

This change:

- adds a pure `compare_route_to_manifest(route_payload, manifest)` function
- returns a metadata-only `ManifestRouteWarningReport`
- warns when selected backend, selected model, or selected route metadata does
not match the manifest
- warns when the manifest uses alias-only model identity
- warns when manifest integrity status is incomplete
- adds focused unit tests

## Non-Scope

- Do not block routing.
- Do not wire this into runtime execution yet.
- Do not probe Ollama, LM Studio, Qwen Cloud, local files, or model artifacts.
- Do not hash model files.
- Do not expand signing.
- Do not mutate the ledger schema.

## Acceptance Criteria

- [x] Add a pure function that compares route metadata to a validated manifest.
- [x] Matching route/backend/model metadata produces no warnings.
- [x] Backend mismatch produces a warning.
- [x] Model mismatch or alias-only identity produces a warning.
- [x] Incomplete manifest integrity status produces a warning.
- [x] Warning report contains only metadata, no raw prompt/data.
- [x] Add focused unit tests.
- [x] No runtime blocking behavior.

## Validation

```powershell
python -m py_compile triage_core\model_manifest.py
python -m pytest tests\test_model_manifest.py -q
python -m pytest -q
git diff --check
git status --short
```
95 changes: 95 additions & 0 deletions tests/test_model_manifest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path

from triage_core.model_manifest import (
compare_route_to_manifest,
load_model_manifest,
validate_model_manifest,
)
Expand Down Expand Up @@ -58,3 +59,97 @@ def test_manifest_missing_required_field_fails():

assert result.is_valid is False
assert any(issue.reason == "missing_required_field" for issue in result.issues)


def test_matching_route_manifest_metadata_has_no_warnings():
manifest = load_model_manifest(_example_path("model_route_manifest_local_ollama.json"))
route_payload = {
"selected_backend": "ollama",
"selected_model": "qwen2.5:7b-instruct-q4_K_M",
"selected_route": "local-ollama-qwen2.5-7b-instruct-q4km",
}

report = compare_route_to_manifest(route_payload, manifest)

assert report.has_warnings is False
assert report.warnings == []


def test_backend_mismatch_produces_warning():
manifest = load_model_manifest(_example_path("model_route_manifest_local_ollama.json"))
route_payload = {
"selected_backend": "qwen_cloud",
"selected_model": "qwen2.5:7b-instruct-q4_K_M",
"selected_route": "local-ollama-qwen2.5-7b-instruct-q4km",
}

report = compare_route_to_manifest(route_payload, manifest)

assert {warning.reason for warning in report.warnings} == {"backend_mismatch"}


def test_model_mismatch_produces_warning():
manifest = load_model_manifest(_example_path("model_route_manifest_local_ollama.json"))
route_payload = {
"selected_backend": "ollama",
"selected_model": "different-model",
"selected_route": "local-ollama-qwen2.5-7b-instruct-q4km",
}

report = compare_route_to_manifest(route_payload, manifest)

assert {warning.reason for warning in report.warnings} == {"model_mismatch"}


def test_alias_only_identity_produces_warning():
manifest = load_model_manifest(
_example_path("model_route_manifest_invalid_alias_only.json")
)
route_payload = {
"selected_backend": "ollama",
"selected_model": "latest",
"selected_route": "alias-only-fast-route",
}

report = compare_route_to_manifest(route_payload, manifest)

reasons = {warning.reason for warning in report.warnings}
assert "alias_only_model_identity" in reasons


def test_incomplete_manifest_integrity_produces_warning():
manifest = load_model_manifest(
_example_path("model_route_manifest_invalid_alias_only.json")
)
route_payload = {
"selected_backend": "ollama",
"selected_model": "latest",
"selected_route": "alias-only-fast-route",
}

report = compare_route_to_manifest(route_payload, manifest)

reasons = {warning.reason for warning in report.warnings}
assert "incomplete_integrity_status" in reasons


def test_warning_report_contains_only_metadata_without_raw_payload_echo():
manifest = load_model_manifest(_example_path("model_route_manifest_local_ollama.json"))
route_payload = {
"selected_backend": "qwen_cloud",
"selected_model": "private-model-alias",
"selected_route": "private-route-alias",
"prompt": "raw private prompt",
"data": "raw private data",
}

report = compare_route_to_manifest(route_payload, manifest)
rendered = "\n".join(
f"{warning.reason} {warning.path} {warning.message}"
for warning in report.warnings
)

assert "raw private prompt" not in rendered
assert "raw private data" not in rendered
assert "private-model-alias" not in rendered
assert "private-route-alias" not in rendered
107 changes: 107 additions & 0 deletions triage_core/model_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ def add_issue(self, reason: str, path: str, message: str) -> None:
self.issues.append(issue)


@dataclass(frozen=True)
class ManifestRouteWarning:
reason: str
path: str
message: str


@dataclass
class ManifestRouteWarningReport:
warnings: list[ManifestRouteWarning] = field(default_factory=list)

@property
def has_warnings(self) -> bool:
return bool(self.warnings)

def add_warning(self, reason: str, path: str, message: str) -> None:
warning = ManifestRouteWarning(reason=reason, path=path, message=message)
if warning not in self.warnings:
self.warnings.append(warning)


def load_model_manifest(manifest_path: str | Path) -> dict[str, Any]:
path = Path(manifest_path)
return json.loads(path.read_text(encoding="utf-8"))
Expand Down Expand Up @@ -126,6 +147,84 @@ def validate_model_manifest(manifest: dict[str, Any]) -> ManifestCheckResult:
return result


def compare_route_to_manifest(
route_payload: dict[str, Any],
manifest: dict[str, Any],
) -> ManifestRouteWarningReport:
report = ManifestRouteWarningReport()

selected_backend = _route_text(
route_payload,
"selected_backend",
"backend_type",
"backend",
)
selected_model = _route_text(
route_payload,
"selected_model",
"model",
"model_id",
"exact_model_id",
)
selected_route = _route_text(
route_payload,
"selected_route",
"recommended_route",
"route_id",
"requested_route",
)

manifest_backend = _string_at_path(manifest, "$.backend.backend_type")
manifest_model = _string_at_path(manifest, "$.model.exact_model_id")
manifest_route = _string_at_path(manifest, "$.route_id")
manifest_mutable_reference = _bool_at_path(manifest, "$.model.mutable_reference")
manifest_integrity_status = _string_at_path(
manifest,
"$.integrity.integrity_status",
)
manifest_provenance_complete = _bool_at_path(
manifest,
"$.integrity.provenance_complete",
)

if selected_backend and manifest_backend and selected_backend != manifest_backend:
report.add_warning(
"backend_mismatch",
"$.backend.backend_type",
"Route selected backend does not match manifest backend_type.",
)

if selected_model and manifest_model and selected_model != manifest_model:
report.add_warning(
"model_mismatch",
"$.model.exact_model_id",
"Route selected model does not match manifest exact_model_id.",
)

if manifest_mutable_reference and manifest_model.lower() in ALIASED_MODEL_IDENTITIES:
report.add_warning(
"alias_only_model_identity",
"$.model.exact_model_id",
"Manifest model identity is an alias-only mutable reference.",
)

if manifest_integrity_status != "complete" or not manifest_provenance_complete:
report.add_warning(
"incomplete_integrity_status",
"$.integrity.integrity_status",
"Manifest integrity status is not complete.",
)

if selected_route and manifest_route and selected_route != manifest_route:
report.add_warning(
"route_mismatch",
"$.route_id",
"Route id does not match manifest route_id.",
)

return report


def summarize_model_manifest_check(
manifest_path: str | Path,
manifest: dict[str, Any],
Expand Down Expand Up @@ -174,3 +273,11 @@ def _bool_at_path(payload: dict[str, Any], path: str) -> bool:
if not found:
return False
return bool(value)


def _route_text(payload: dict[str, Any], *keys: str) -> str:
for key in keys:
value = payload.get(key)
if value is not None:
return str(value)
return ""
Loading