agent-assure 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_assure/__init__.py +4 -0
- agent_assure/artifact_io.py +26 -0
- agent_assure/authoring/__init__.py +4 -0
- agent_assure/authoring/compiler.py +92 -0
- agent_assure/authoring/string_fields.py +15 -0
- agent_assure/authoring/yaml_lint.py +9 -0
- agent_assure/authoring/yaml_loader.py +4 -0
- agent_assure/authoring/yaml_nodes.py +84 -0
- agent_assure/canonical/__init__.py +11 -0
- agent_assure/canonical/digests.py +11 -0
- agent_assure/canonical/hmac_tokens.py +21 -0
- agent_assure/canonical/jcs.py +12 -0
- agent_assure/canonical/manifest.py +10 -0
- agent_assure/canonical/normalize.py +70 -0
- agent_assure/canonical/projection.py +3 -0
- agent_assure/ci.py +444 -0
- agent_assure/cli/__init__.py +1 -0
- agent_assure/cli/ci_cmd.py +116 -0
- agent_assure/cli/compare_cmd.py +161 -0
- agent_assure/cli/demo_cmd.py +73 -0
- agent_assure/cli/diff_cmd.py +176 -0
- agent_assure/cli/evaluate_cmd.py +147 -0
- agent_assure/cli/init_cmd.py +10 -0
- agent_assure/cli/live_cmd.py +222 -0
- agent_assure/cli/main.py +58 -0
- agent_assure/cli/otel_cmd.py +119 -0
- agent_assure/cli/packet_cmd.py +97 -0
- agent_assure/cli/release_cmd.py +103 -0
- agent_assure/cli/schema_cmd.py +19 -0
- agent_assure/cli/suite_cmd.py +132 -0
- agent_assure/cli/validate_cmd.py +19 -0
- agent_assure/cli/waivers.py +24 -0
- agent_assure/compare/__init__.py +1 -0
- agent_assure/compare/case_map.py +21 -0
- agent_assure/compare/classifications.py +39 -0
- agent_assure/compare/invariant_diff.py +186 -0
- agent_assure/compare/provenance_diff.py +52 -0
- agent_assure/compare/runsets.py +402 -0
- agent_assure/demo/__init__.py +2 -0
- agent_assure/demo/common.py +344 -0
- agent_assure/demo/flagship.py +459 -0
- agent_assure/evaluation/__init__.py +1 -0
- agent_assure/evaluation/aggregation.py +6 -0
- agent_assure/evaluation/applicability.py +7 -0
- agent_assure/evaluation/evaluator.py +261 -0
- agent_assure/evaluation/expectations.py +45 -0
- agent_assure/evaluation/invariants.py +219 -0
- agent_assure/evaluation/resolver.py +5 -0
- agent_assure/examples/__init__.py +1 -0
- agent_assure/examples/expense_approval_minimal/README.md +16 -0
- agent_assure/examples/expense_approval_minimal/__init__.py +1 -0
- agent_assure/examples/expense_approval_minimal/fixtures/shared/model_outputs/exp-001.json +8 -0
- agent_assure/examples/expense_approval_minimal/fixtures/shared/model_outputs/exp-002.json +8 -0
- agent_assure/examples/expense_approval_minimal/fixtures/shared/model_outputs/exp-003.json +8 -0
- agent_assure/examples/expense_approval_minimal/fixtures/shared/requests/exp-001.json +6 -0
- agent_assure/examples/expense_approval_minimal/fixtures/shared/requests/exp-002.json +6 -0
- agent_assure/examples/expense_approval_minimal/fixtures/shared/requests/exp-003.json +6 -0
- agent_assure/examples/expense_approval_minimal/fixtures/shared/tool_outputs/exp-001.json +15 -0
- agent_assure/examples/expense_approval_minimal/fixtures/shared/tool_outputs/exp-002.json +15 -0
- agent_assure/examples/expense_approval_minimal/fixtures/shared/tool_outputs/exp-003.json +15 -0
- agent_assure/examples/expense_approval_minimal/runner.py +64 -0
- agent_assure/examples/expense_approval_minimal/suite.yaml +57 -0
- agent_assure/examples/expense_approval_minimal/variants/baseline.yaml +11 -0
- agent_assure/examples/expense_approval_minimal/variants/candidate_provider_policy.yaml +11 -0
- agent_assure/examples/prior_auth_synthetic/README.md +40 -0
- agent_assure/examples/prior_auth_synthetic/__init__.py +1 -0
- agent_assure/examples/prior_auth_synthetic/app.py +138 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/ambiguous-case.json +8 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/conflicting-evidence.json +8 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/fake-phi-redaction.json +8 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/forbidden-provider.json +8 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/missing-documentation.json +8 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/prompt-injection-note.json +8 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/shared-source-multi-claim.json +8 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/straightforward-approval.json +8 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/straightforward-denial.json +8 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/tool-failure.json +8 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/ambiguous-case.json +6 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/conflicting-evidence.json +6 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/fake-phi-redaction.json +10 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/forbidden-provider.json +6 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/missing-documentation.json +6 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/prompt-injection-note.json +6 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/shared-source-multi-claim.json +6 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/straightforward-approval.json +6 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/straightforward-denial.json +6 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/tool-failure.json +6 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/ambiguous-case.json +15 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/conflicting-evidence.json +15 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/fake-phi-redaction.json +15 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/forbidden-provider.json +15 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/missing-documentation.json +15 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/prompt-injection-note.json +16 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/shared-source-multi-claim.json +24 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/straightforward-approval.json +15 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/straightforward-denial.json +15 -0
- agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/tool-failure.json +15 -0
- agent_assure/examples/prior_auth_synthetic/runner.py +64 -0
- agent_assure/examples/prior_auth_synthetic/suite.yaml +134 -0
- agent_assure/examples/prior_auth_synthetic/variants/baseline.yaml +11 -0
- agent_assure/examples/prior_auth_synthetic/variants/candidate_evidence_normalization.yaml +10 -0
- agent_assure/examples/prior_auth_synthetic/variants/candidate_provider_policy.yaml +11 -0
- agent_assure/examples/prior_auth_synthetic/variants/candidate_smoke_fail.yaml +12 -0
- agent_assure/fixtures/__init__.py +32 -0
- agent_assure/fixtures/loader.py +42 -0
- agent_assure/fixtures/manifest.py +137 -0
- agent_assure/fixtures/resolver.py +62 -0
- agent_assure/live/__init__.py +2 -0
- agent_assure/live/adapters.py +497 -0
- agent_assure/live/advanced.py +587 -0
- agent_assure/live/comparison.py +452 -0
- agent_assure/live/config.py +125 -0
- agent_assure/live/drift.py +866 -0
- agent_assure/live/intervals.py +230 -0
- agent_assure/live/output_contract.py +78 -0
- agent_assure/live/paths.py +19 -0
- agent_assure/live/primitives.py +73 -0
- agent_assure/live/runner.py +783 -0
- agent_assure/live/statistics.py +629 -0
- agent_assure/live/trajectory.py +773 -0
- agent_assure/policies/__init__.py +1 -0
- agent_assure/policies/base.py +170 -0
- agent_assure/policies/catalog.py +58 -0
- agent_assure/policies/evidence.py +57 -0
- agent_assure/policies/human_review.py +25 -0
- agent_assure/policies/injection.py +30 -0
- agent_assure/policies/output_schema.py +27 -0
- agent_assure/policies/privacy.py +65 -0
- agent_assure/policies/providers.py +60 -0
- agent_assure/policies/review_boundary.py +10 -0
- agent_assure/policies/runtime.py +22 -0
- agent_assure/policies/tools.py +45 -0
- agent_assure/privacy/__init__.py +18 -0
- agent_assure/privacy/detectors.py +36 -0
- agent_assure/privacy/redaction.py +121 -0
- agent_assure/privacy/safe_errors.py +51 -0
- agent_assure/release_evidence.py +487 -0
- agent_assure/reporting/__init__.py +1 -0
- agent_assure/reporting/console.py +137 -0
- agent_assure/reporting/environment.py +191 -0
- agent_assure/reporting/evidence_diff_html.py +1035 -0
- agent_assure/reporting/json_report.py +73 -0
- agent_assure/reporting/live.py +386 -0
- agent_assure/reporting/markdown.py +227 -0
- agent_assure/reporting/packet.py +186 -0
- agent_assure/reporting/sbom.py +126 -0
- agent_assure/runner/__init__.py +35 -0
- agent_assure/runner/clock.py +17 -0
- agent_assure/runner/emergency.py +1 -0
- agent_assure/runner/evidence.py +107 -0
- agent_assure/runner/fixture_runner.py +247 -0
- agent_assure/runner/fixture_values.py +20 -0
- agent_assure/runner/governance_controls.py +109 -0
- agent_assure/runner/ids.py +20 -0
- agent_assure/runner/records.py +91 -0
- agent_assure/runner/registry.py +54 -0
- agent_assure/runner/subprocess_harness.py +287 -0
- agent_assure/schema/__init__.py +98 -0
- agent_assure/schema/base.py +38 -0
- agent_assure/schema/common.py +105 -0
- agent_assure/schema/comparison.py +47 -0
- agent_assure/schema/environment.py +32 -0
- agent_assure/schema/evaluation.py +48 -0
- agent_assure/schema/expectation.py +66 -0
- agent_assure/schema/export.py +83 -0
- agent_assure/schema/live.py +1310 -0
- agent_assure/schema/packet.py +37 -0
- agent_assure/schema/provenance.py +18 -0
- agent_assure/schema/release.py +61 -0
- agent_assure/schema/run.py +237 -0
- agent_assure/schema/runtime.py +46 -0
- agent_assure/schema/suite.py +128 -0
- agent_assure/schema/telemetry.py +50 -0
- agent_assure/schema/validation.py +29 -0
- agent_assure/schema_resources/__init__.py +1 -0
- agent_assure/schema_resources/v0.1.0/agent-run-record.schema.json +819 -0
- agent_assure/schema_resources/v0.1.0/comparison-report.schema.json +753 -0
- agent_assure/schema_resources/v0.1.0/comparison-summary.schema.json +245 -0
- agent_assure/schema_resources/v0.1.0/compiled-suite.schema.json +323 -0
- agent_assure/schema_resources/v0.1.0/environment-info.schema.json +148 -0
- agent_assure/schema_resources/v0.1.0/evaluation-report.schema.json +476 -0
- agent_assure/schema_resources/v0.1.0/evaluation-summary.schema.json +286 -0
- agent_assure/schema_resources/v0.1.0/evidence-packet.schema.json +581 -0
- agent_assure/schema_resources/v0.1.0/expectation-change-record.schema.json +97 -0
- agent_assure/schema_resources/v0.1.0/expectation.schema.json +129 -0
- agent_assure/schema_resources/v0.1.0/fixture-manifest.schema.json +92 -0
- agent_assure/schema_resources/v0.1.0/live-comparison-report.schema.json +328 -0
- agent_assure/schema_resources/v0.1.0/live-evaluation-report.schema.json +860 -0
- agent_assure/schema_resources/v0.1.0/live-protocol-record.schema.json +353 -0
- agent_assure/schema_resources/v0.1.0/release-artifact-manifest.schema.json +233 -0
- agent_assure/schema_resources/v0.1.0/release-digest-replay.schema.json +113 -0
- agent_assure/schema_resources/v0.1.0/run-set.schema.json +921 -0
- agent_assure/schema_resources/v0.1.0/span-plan.schema.json +131 -0
- agent_assure/schema_resources/v0.2.0/agent-run-record.schema.json +862 -0
- agent_assure/schema_resources/v0.2.0/comparison-report.schema.json +753 -0
- agent_assure/schema_resources/v0.2.0/comparison-summary.schema.json +245 -0
- agent_assure/schema_resources/v0.2.0/compiled-suite.schema.json +323 -0
- agent_assure/schema_resources/v0.2.0/emergency-process-record.schema.json +260 -0
- agent_assure/schema_resources/v0.2.0/environment-info.schema.json +148 -0
- agent_assure/schema_resources/v0.2.0/evaluation-report.schema.json +476 -0
- agent_assure/schema_resources/v0.2.0/evaluation-summary.schema.json +286 -0
- agent_assure/schema_resources/v0.2.0/evidence-packet.schema.json +581 -0
- agent_assure/schema_resources/v0.2.0/expectation-change-record.schema.json +97 -0
- agent_assure/schema_resources/v0.2.0/expectation.schema.json +129 -0
- agent_assure/schema_resources/v0.2.0/fixture-manifest.schema.json +92 -0
- agent_assure/schema_resources/v0.2.0/live-comparison-report.schema.json +496 -0
- agent_assure/schema_resources/v0.2.0/live-drift-report.schema.json +1011 -0
- agent_assure/schema_resources/v0.2.0/live-evaluation-report.schema.json +1361 -0
- agent_assure/schema_resources/v0.2.0/live-protocol-record.schema.json +1097 -0
- agent_assure/schema_resources/v0.2.0/live-trajectory-report.schema.json +770 -0
- agent_assure/schema_resources/v0.2.0/release-artifact-manifest.schema.json +233 -0
- agent_assure/schema_resources/v0.2.0/release-digest-replay.schema.json +113 -0
- agent_assure/schema_resources/v0.2.0/run-set.schema.json +1230 -0
- agent_assure/schema_resources/v0.2.0/span-plan.schema.json +156 -0
- agent_assure/schema_resources/v0.3.0/agent-run-record.schema.json +862 -0
- agent_assure/schema_resources/v0.3.0/comparison-report.schema.json +753 -0
- agent_assure/schema_resources/v0.3.0/comparison-summary.schema.json +245 -0
- agent_assure/schema_resources/v0.3.0/compiled-suite.schema.json +323 -0
- agent_assure/schema_resources/v0.3.0/emergency-process-record.schema.json +260 -0
- agent_assure/schema_resources/v0.3.0/environment-info.schema.json +148 -0
- agent_assure/schema_resources/v0.3.0/evaluation-report.schema.json +476 -0
- agent_assure/schema_resources/v0.3.0/evaluation-summary.schema.json +286 -0
- agent_assure/schema_resources/v0.3.0/evidence-packet.schema.json +581 -0
- agent_assure/schema_resources/v0.3.0/expectation-change-record.schema.json +97 -0
- agent_assure/schema_resources/v0.3.0/expectation.schema.json +129 -0
- agent_assure/schema_resources/v0.3.0/fixture-manifest.schema.json +92 -0
- agent_assure/schema_resources/v0.3.0/live-comparison-report.schema.json +496 -0
- agent_assure/schema_resources/v0.3.0/live-drift-report.schema.json +1011 -0
- agent_assure/schema_resources/v0.3.0/live-evaluation-report.schema.json +1361 -0
- agent_assure/schema_resources/v0.3.0/live-protocol-record.schema.json +1097 -0
- agent_assure/schema_resources/v0.3.0/live-trajectory-report.schema.json +770 -0
- agent_assure/schema_resources/v0.3.0/release-artifact-manifest.schema.json +233 -0
- agent_assure/schema_resources/v0.3.0/release-digest-replay.schema.json +113 -0
- agent_assure/schema_resources/v0.3.0/run-set.schema.json +1230 -0
- agent_assure/schema_resources/v0.3.0/span-plan.schema.json +156 -0
- agent_assure/telemetry/__init__.py +15 -0
- agent_assure/telemetry/context.py +55 -0
- agent_assure/telemetry/otel_mapping.py +78 -0
- agent_assure/telemetry/otel_sdk.py +138 -0
- agent_assure/telemetry/privacy_filter.py +7 -0
- agent_assure/telemetry/semconv_lock.py +4 -0
- agent_assure/telemetry/span_plan.py +3 -0
- agent_assure-0.3.0.dist-info/METADATA +376 -0
- agent_assure-0.3.0.dist-info/RECORD +247 -0
- agent_assure-0.3.0.dist-info/WHEEL +4 -0
- agent_assure-0.3.0.dist-info/entry_points.txt +2 -0
- agent_assure-0.3.0.dist-info/licenses/LICENSE +21 -0
agent_assure/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def file_sha256(path: Path) -> str:
|
|
9
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def git_output(project_root: Path, *args: str) -> str | None:
|
|
13
|
+
try:
|
|
14
|
+
result = subprocess.run(
|
|
15
|
+
["git", *args],
|
|
16
|
+
cwd=project_root,
|
|
17
|
+
check=False,
|
|
18
|
+
capture_output=True,
|
|
19
|
+
text=True,
|
|
20
|
+
timeout=5,
|
|
21
|
+
)
|
|
22
|
+
except (OSError, subprocess.SubprocessError):
|
|
23
|
+
return None
|
|
24
|
+
if result.returncode != 0:
|
|
25
|
+
return None
|
|
26
|
+
return result.stdout.strip() or None
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from agent_assure.authoring.yaml_nodes import LoadedYaml, load_yaml_nodes
|
|
7
|
+
from agent_assure.canonical.digests import sha256_hexdigest
|
|
8
|
+
from agent_assure.schema.expectation import Expectation
|
|
9
|
+
from agent_assure.schema.suite import CompiledSuite, SuiteCase, SuiteDefaults
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def compile_suite(path: Path) -> CompiledSuite:
|
|
13
|
+
loaded = load_yaml_nodes(path)
|
|
14
|
+
return compile_loaded_suite(loaded, source_digest=sha256_hexdigest(loaded.data))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def compile_loaded_suite(loaded: LoadedYaml, source_digest: str) -> CompiledSuite:
|
|
18
|
+
data = loaded.data
|
|
19
|
+
defaults_data = dict(_mapping(data.get("defaults", {})))
|
|
20
|
+
expectation_defaults = _mapping(
|
|
21
|
+
defaults_data.pop("expectation", defaults_data.pop("expectation_defaults", {}))
|
|
22
|
+
)
|
|
23
|
+
defaults = SuiteDefaults(**defaults_data)
|
|
24
|
+
cases: list[SuiteCase] = []
|
|
25
|
+
expectations: list[Expectation] = []
|
|
26
|
+
for case_data in _sequence(data.get("cases", ())):
|
|
27
|
+
case_map = _mapping(case_data)
|
|
28
|
+
expectation_data = _merge_expectation_defaults(
|
|
29
|
+
expectation_defaults,
|
|
30
|
+
_mapping(case_map.get("expectation", {})),
|
|
31
|
+
)
|
|
32
|
+
expectation_data.setdefault("case_id", str(case_map["case_id"]))
|
|
33
|
+
expectation_data.setdefault("expectation_id", f"{case_map['case_id']}:expectation")
|
|
34
|
+
expectation_without_digest = Expectation(**expectation_data)
|
|
35
|
+
expectation = expectation_without_digest.model_copy(
|
|
36
|
+
update={
|
|
37
|
+
"expectation_digest": sha256_hexdigest(
|
|
38
|
+
expectation_without_digest.model_dump(
|
|
39
|
+
mode="json",
|
|
40
|
+
exclude={"expectation_digest"},
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
case = SuiteCase(
|
|
46
|
+
case_id=str(case_map["case_id"]),
|
|
47
|
+
title=str(case_map["title"]),
|
|
48
|
+
expectation_id=expectation.expectation_id,
|
|
49
|
+
fixture_id=_optional_string(case_map.get("fixture_id")),
|
|
50
|
+
tags=tuple(str(tag) for tag in _sequence(case_map.get("tags", ()))),
|
|
51
|
+
)
|
|
52
|
+
cases.append(case)
|
|
53
|
+
expectations.append(expectation)
|
|
54
|
+
return CompiledSuite(
|
|
55
|
+
suite_id=str(data["suite_id"]),
|
|
56
|
+
suite_version=str(data["suite_version"]),
|
|
57
|
+
defaults=defaults,
|
|
58
|
+
cases=tuple(cases),
|
|
59
|
+
resolved_expectations=tuple(expectations),
|
|
60
|
+
source_digest=source_digest,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _mapping(value: Any) -> dict[str, Any]:
|
|
65
|
+
if not isinstance(value, dict):
|
|
66
|
+
raise TypeError("expected mapping")
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _sequence(value: Any) -> tuple[Any, ...]:
|
|
71
|
+
if not isinstance(value, tuple | list):
|
|
72
|
+
raise TypeError("expected sequence")
|
|
73
|
+
return tuple(value)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _optional_string(value: Any) -> str | None:
|
|
77
|
+
if value is None:
|
|
78
|
+
return None
|
|
79
|
+
return str(value)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _merge_expectation_defaults(
|
|
83
|
+
defaults: dict[str, Any],
|
|
84
|
+
overrides: dict[str, Any],
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
merged = dict(defaults)
|
|
87
|
+
for key, value in overrides.items():
|
|
88
|
+
if isinstance(merged.get(key), dict) and isinstance(value, dict):
|
|
89
|
+
merged[key] = _merge_expectation_defaults(merged[key], value)
|
|
90
|
+
continue
|
|
91
|
+
merged[key] = value
|
|
92
|
+
return merged
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
SEMANTIC_STRING_FIELDS = {
|
|
2
|
+
"suite_id",
|
|
3
|
+
"suite_version",
|
|
4
|
+
"case_id",
|
|
5
|
+
"expectation_id",
|
|
6
|
+
"expected_recommendation",
|
|
7
|
+
"allowed_outcomes",
|
|
8
|
+
"forbidden_outcomes",
|
|
9
|
+
"required_evidence_refs",
|
|
10
|
+
"material_claim_ids",
|
|
11
|
+
"allowed_providers",
|
|
12
|
+
"forbidden_providers",
|
|
13
|
+
"allowed_tools",
|
|
14
|
+
"forbidden_tools",
|
|
15
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unicodedata
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class YamlWarning:
|
|
13
|
+
path: str
|
|
14
|
+
message: str
|
|
15
|
+
line: int
|
|
16
|
+
column: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class LoadedYaml:
|
|
21
|
+
data: dict[str, Any]
|
|
22
|
+
warnings: tuple[YamlWarning, ...]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
AMBIGUOUS_TAGS = {
|
|
26
|
+
"tag:yaml.org,2002:int",
|
|
27
|
+
"tag:yaml.org,2002:float",
|
|
28
|
+
"tag:yaml.org,2002:timestamp",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_yaml_nodes(path: Path) -> LoadedYaml:
|
|
33
|
+
text = path.read_text(encoding="utf-8")
|
|
34
|
+
node = yaml.compose(text)
|
|
35
|
+
warnings: list[YamlWarning] = []
|
|
36
|
+
data = _convert_node(node, "$", warnings)
|
|
37
|
+
if not isinstance(data, dict):
|
|
38
|
+
raise ValueError("suite YAML root must be a mapping")
|
|
39
|
+
return LoadedYaml(data=data, warnings=tuple(warnings))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _convert_node(node: yaml.Node | None, path: str, warnings: list[YamlWarning]) -> Any:
|
|
43
|
+
if node is None:
|
|
44
|
+
return {}
|
|
45
|
+
if isinstance(node, yaml.MappingNode):
|
|
46
|
+
result: dict[str, Any] = {}
|
|
47
|
+
for key_node, value_node in node.value:
|
|
48
|
+
key = str(_convert_node(key_node, path, warnings))
|
|
49
|
+
if key in result:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"duplicate YAML mapping key at {path}.{key}: "
|
|
52
|
+
f"line {key_node.start_mark.line + 1}, "
|
|
53
|
+
f"column {key_node.start_mark.column + 1}"
|
|
54
|
+
)
|
|
55
|
+
result[key] = _convert_node(value_node, f"{path}.{key}", warnings)
|
|
56
|
+
return result
|
|
57
|
+
if isinstance(node, yaml.SequenceNode):
|
|
58
|
+
return tuple(_convert_node(item, f"{path}[]", warnings) for item in node.value)
|
|
59
|
+
if isinstance(node, yaml.ScalarNode):
|
|
60
|
+
if unicodedata.normalize("NFC", node.value) != node.value:
|
|
61
|
+
warnings.append(
|
|
62
|
+
YamlWarning(
|
|
63
|
+
path=path,
|
|
64
|
+
message="string is not NFC-normalized",
|
|
65
|
+
line=node.start_mark.line + 1,
|
|
66
|
+
column=node.start_mark.column + 1,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
if node.tag in AMBIGUOUS_TAGS:
|
|
70
|
+
warnings.append(
|
|
71
|
+
YamlWarning(
|
|
72
|
+
path=path,
|
|
73
|
+
message=f"ambiguous scalar preserved as string: {node.value!r}",
|
|
74
|
+
line=node.start_mark.line + 1,
|
|
75
|
+
column=node.start_mark.column + 1,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
return node.value
|
|
79
|
+
if node.tag == "tag:yaml.org,2002:bool":
|
|
80
|
+
return node.value.lower() in {"true", "yes", "on"}
|
|
81
|
+
if node.tag == "tag:yaml.org,2002:null":
|
|
82
|
+
return None
|
|
83
|
+
return node.value
|
|
84
|
+
raise TypeError(f"unsupported YAML node: {type(node).__name__}")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from agent_assure.canonical.digests import sha256_hexdigest
|
|
2
|
+
from agent_assure.canonical.hmac_tokens import hmac_sha256_token, verify_hmac_token
|
|
3
|
+
from agent_assure.canonical.normalize import digest_projection, normalize_decimal
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"digest_projection",
|
|
7
|
+
"hmac_sha256_token",
|
|
8
|
+
"normalize_decimal",
|
|
9
|
+
"sha256_hexdigest",
|
|
10
|
+
"verify_hmac_token",
|
|
11
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from agent_assure.canonical.jcs import canonical_bytes
|
|
7
|
+
from agent_assure.canonical.normalize import digest_projection
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def sha256_hexdigest(value: Any) -> str:
|
|
11
|
+
return hashlib.sha256(canonical_bytes(digest_projection(value))).hexdigest()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def hmac_sha256_token(value: str, key: bytes) -> str:
|
|
8
|
+
_validate_hmac_key(key)
|
|
9
|
+
return hmac.new(key, value.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def verify_hmac_token(received_token: str, value: str, key: bytes) -> bool:
|
|
13
|
+
expected_token = hmac_sha256_token(value, key)
|
|
14
|
+
return hmac.compare_digest(received_token, expected_token)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _validate_hmac_key(key: bytes) -> None:
|
|
18
|
+
if not isinstance(key, bytes):
|
|
19
|
+
raise TypeError("HMAC key must be bytes")
|
|
20
|
+
if not key:
|
|
21
|
+
raise ValueError("HMAC key must not be empty")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def posix_manifest_path(path: Path, root: Path) -> str:
|
|
7
|
+
resolved = path.resolve()
|
|
8
|
+
resolved_root = root.resolve()
|
|
9
|
+
relative = resolved.relative_to(resolved_root)
|
|
10
|
+
return relative.as_posix()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unicodedata
|
|
4
|
+
from decimal import Decimal, localcontext
|
|
5
|
+
from math import isfinite
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_assure.schema.common import ReasonCode
|
|
9
|
+
|
|
10
|
+
DECIMAL_QUANTUM = Decimal("0.000001")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CanonicalizationError(ValueError):
|
|
14
|
+
def __init__(self, reason_code: ReasonCode, message: str) -> None:
|
|
15
|
+
self.reason_code = reason_code
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_decimal(value: Decimal | str) -> str:
|
|
20
|
+
decimal = Decimal(str(value))
|
|
21
|
+
if not decimal.is_finite():
|
|
22
|
+
raise CanonicalizationError(ReasonCode.NON_FINITE_NUMBER, "decimal is not finite")
|
|
23
|
+
quantum_exponent = DECIMAL_QUANTUM.as_tuple().exponent
|
|
24
|
+
if not isinstance(quantum_exponent, int):
|
|
25
|
+
raise CanonicalizationError(ReasonCode.NON_FINITE_NUMBER, "decimal quantum is not finite")
|
|
26
|
+
fractional_places = abs(quantum_exponent)
|
|
27
|
+
integer_digits = max(decimal.adjusted() + 1, 1)
|
|
28
|
+
with localcontext() as context:
|
|
29
|
+
context.prec = max(context.prec, integer_digits + fractional_places + 1)
|
|
30
|
+
return format(decimal.quantize(DECIMAL_QUANTUM), "f")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ensure_nfc(value: str) -> str:
|
|
34
|
+
if unicodedata.normalize("NFC", value) != value:
|
|
35
|
+
raise CanonicalizationError(ReasonCode.NON_NFC_STRING, "string is not NFC-normalized")
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def digest_projection(value: Any) -> Any:
|
|
40
|
+
"""Project typed values into deterministic canonical digest material.
|
|
41
|
+
|
|
42
|
+
Explicit ``None`` values are preserved as identity data. Digest roles that
|
|
43
|
+
need missing fields and null fields to be equivalent must apply a
|
|
44
|
+
role-specific projection before using this generic path.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(value, Decimal):
|
|
47
|
+
return normalize_decimal(value)
|
|
48
|
+
if isinstance(value, str):
|
|
49
|
+
return ensure_nfc(value)
|
|
50
|
+
if isinstance(value, bool) or value is None or isinstance(value, int):
|
|
51
|
+
return value
|
|
52
|
+
if isinstance(value, float):
|
|
53
|
+
if not isfinite(value):
|
|
54
|
+
raise CanonicalizationError(ReasonCode.NON_FINITE_NUMBER, "float is not finite")
|
|
55
|
+
raise TypeError("float values must be converted to Decimal before digest projection")
|
|
56
|
+
if isinstance(value, tuple | list):
|
|
57
|
+
return [digest_projection(item) for item in value]
|
|
58
|
+
if isinstance(value, dict):
|
|
59
|
+
projected_items: list[tuple[str, Any]] = []
|
|
60
|
+
seen_keys: set[str] = set()
|
|
61
|
+
for key, item in value.items():
|
|
62
|
+
projected_key = ensure_nfc(str(key))
|
|
63
|
+
if projected_key in seen_keys:
|
|
64
|
+
raise TypeError(f"duplicate projected object key: {projected_key!r}")
|
|
65
|
+
seen_keys.add(projected_key)
|
|
66
|
+
projected_items.append((projected_key, digest_projection(item)))
|
|
67
|
+
return {key: item for key, item in sorted(projected_items, key=lambda pair: pair[0])}
|
|
68
|
+
if hasattr(value, "model_dump"):
|
|
69
|
+
return digest_projection(value.model_dump(mode="json"))
|
|
70
|
+
raise TypeError(f"unsupported digest projection type: {type(value).__name__}")
|