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.
Files changed (247) hide show
  1. agent_assure/__init__.py +4 -0
  2. agent_assure/artifact_io.py +26 -0
  3. agent_assure/authoring/__init__.py +4 -0
  4. agent_assure/authoring/compiler.py +92 -0
  5. agent_assure/authoring/string_fields.py +15 -0
  6. agent_assure/authoring/yaml_lint.py +9 -0
  7. agent_assure/authoring/yaml_loader.py +4 -0
  8. agent_assure/authoring/yaml_nodes.py +84 -0
  9. agent_assure/canonical/__init__.py +11 -0
  10. agent_assure/canonical/digests.py +11 -0
  11. agent_assure/canonical/hmac_tokens.py +21 -0
  12. agent_assure/canonical/jcs.py +12 -0
  13. agent_assure/canonical/manifest.py +10 -0
  14. agent_assure/canonical/normalize.py +70 -0
  15. agent_assure/canonical/projection.py +3 -0
  16. agent_assure/ci.py +444 -0
  17. agent_assure/cli/__init__.py +1 -0
  18. agent_assure/cli/ci_cmd.py +116 -0
  19. agent_assure/cli/compare_cmd.py +161 -0
  20. agent_assure/cli/demo_cmd.py +73 -0
  21. agent_assure/cli/diff_cmd.py +176 -0
  22. agent_assure/cli/evaluate_cmd.py +147 -0
  23. agent_assure/cli/init_cmd.py +10 -0
  24. agent_assure/cli/live_cmd.py +222 -0
  25. agent_assure/cli/main.py +58 -0
  26. agent_assure/cli/otel_cmd.py +119 -0
  27. agent_assure/cli/packet_cmd.py +97 -0
  28. agent_assure/cli/release_cmd.py +103 -0
  29. agent_assure/cli/schema_cmd.py +19 -0
  30. agent_assure/cli/suite_cmd.py +132 -0
  31. agent_assure/cli/validate_cmd.py +19 -0
  32. agent_assure/cli/waivers.py +24 -0
  33. agent_assure/compare/__init__.py +1 -0
  34. agent_assure/compare/case_map.py +21 -0
  35. agent_assure/compare/classifications.py +39 -0
  36. agent_assure/compare/invariant_diff.py +186 -0
  37. agent_assure/compare/provenance_diff.py +52 -0
  38. agent_assure/compare/runsets.py +402 -0
  39. agent_assure/demo/__init__.py +2 -0
  40. agent_assure/demo/common.py +344 -0
  41. agent_assure/demo/flagship.py +459 -0
  42. agent_assure/evaluation/__init__.py +1 -0
  43. agent_assure/evaluation/aggregation.py +6 -0
  44. agent_assure/evaluation/applicability.py +7 -0
  45. agent_assure/evaluation/evaluator.py +261 -0
  46. agent_assure/evaluation/expectations.py +45 -0
  47. agent_assure/evaluation/invariants.py +219 -0
  48. agent_assure/evaluation/resolver.py +5 -0
  49. agent_assure/examples/__init__.py +1 -0
  50. agent_assure/examples/expense_approval_minimal/README.md +16 -0
  51. agent_assure/examples/expense_approval_minimal/__init__.py +1 -0
  52. agent_assure/examples/expense_approval_minimal/fixtures/shared/model_outputs/exp-001.json +8 -0
  53. agent_assure/examples/expense_approval_minimal/fixtures/shared/model_outputs/exp-002.json +8 -0
  54. agent_assure/examples/expense_approval_minimal/fixtures/shared/model_outputs/exp-003.json +8 -0
  55. agent_assure/examples/expense_approval_minimal/fixtures/shared/requests/exp-001.json +6 -0
  56. agent_assure/examples/expense_approval_minimal/fixtures/shared/requests/exp-002.json +6 -0
  57. agent_assure/examples/expense_approval_minimal/fixtures/shared/requests/exp-003.json +6 -0
  58. agent_assure/examples/expense_approval_minimal/fixtures/shared/tool_outputs/exp-001.json +15 -0
  59. agent_assure/examples/expense_approval_minimal/fixtures/shared/tool_outputs/exp-002.json +15 -0
  60. agent_assure/examples/expense_approval_minimal/fixtures/shared/tool_outputs/exp-003.json +15 -0
  61. agent_assure/examples/expense_approval_minimal/runner.py +64 -0
  62. agent_assure/examples/expense_approval_minimal/suite.yaml +57 -0
  63. agent_assure/examples/expense_approval_minimal/variants/baseline.yaml +11 -0
  64. agent_assure/examples/expense_approval_minimal/variants/candidate_provider_policy.yaml +11 -0
  65. agent_assure/examples/prior_auth_synthetic/README.md +40 -0
  66. agent_assure/examples/prior_auth_synthetic/__init__.py +1 -0
  67. agent_assure/examples/prior_auth_synthetic/app.py +138 -0
  68. agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/ambiguous-case.json +8 -0
  69. agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/conflicting-evidence.json +8 -0
  70. agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/fake-phi-redaction.json +8 -0
  71. agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/forbidden-provider.json +8 -0
  72. agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/missing-documentation.json +8 -0
  73. agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/prompt-injection-note.json +8 -0
  74. agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/shared-source-multi-claim.json +8 -0
  75. agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/straightforward-approval.json +8 -0
  76. agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/straightforward-denial.json +8 -0
  77. agent_assure/examples/prior_auth_synthetic/fixtures/shared/model_outputs/tool-failure.json +8 -0
  78. agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/ambiguous-case.json +6 -0
  79. agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/conflicting-evidence.json +6 -0
  80. agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/fake-phi-redaction.json +10 -0
  81. agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/forbidden-provider.json +6 -0
  82. agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/missing-documentation.json +6 -0
  83. agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/prompt-injection-note.json +6 -0
  84. agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/shared-source-multi-claim.json +6 -0
  85. agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/straightforward-approval.json +6 -0
  86. agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/straightforward-denial.json +6 -0
  87. agent_assure/examples/prior_auth_synthetic/fixtures/shared/requests/tool-failure.json +6 -0
  88. agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/ambiguous-case.json +15 -0
  89. agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/conflicting-evidence.json +15 -0
  90. agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/fake-phi-redaction.json +15 -0
  91. agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/forbidden-provider.json +15 -0
  92. agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/missing-documentation.json +15 -0
  93. agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/prompt-injection-note.json +16 -0
  94. agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/shared-source-multi-claim.json +24 -0
  95. agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/straightforward-approval.json +15 -0
  96. agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/straightforward-denial.json +15 -0
  97. agent_assure/examples/prior_auth_synthetic/fixtures/shared/tool_outputs/tool-failure.json +15 -0
  98. agent_assure/examples/prior_auth_synthetic/runner.py +64 -0
  99. agent_assure/examples/prior_auth_synthetic/suite.yaml +134 -0
  100. agent_assure/examples/prior_auth_synthetic/variants/baseline.yaml +11 -0
  101. agent_assure/examples/prior_auth_synthetic/variants/candidate_evidence_normalization.yaml +10 -0
  102. agent_assure/examples/prior_auth_synthetic/variants/candidate_provider_policy.yaml +11 -0
  103. agent_assure/examples/prior_auth_synthetic/variants/candidate_smoke_fail.yaml +12 -0
  104. agent_assure/fixtures/__init__.py +32 -0
  105. agent_assure/fixtures/loader.py +42 -0
  106. agent_assure/fixtures/manifest.py +137 -0
  107. agent_assure/fixtures/resolver.py +62 -0
  108. agent_assure/live/__init__.py +2 -0
  109. agent_assure/live/adapters.py +497 -0
  110. agent_assure/live/advanced.py +587 -0
  111. agent_assure/live/comparison.py +452 -0
  112. agent_assure/live/config.py +125 -0
  113. agent_assure/live/drift.py +866 -0
  114. agent_assure/live/intervals.py +230 -0
  115. agent_assure/live/output_contract.py +78 -0
  116. agent_assure/live/paths.py +19 -0
  117. agent_assure/live/primitives.py +73 -0
  118. agent_assure/live/runner.py +783 -0
  119. agent_assure/live/statistics.py +629 -0
  120. agent_assure/live/trajectory.py +773 -0
  121. agent_assure/policies/__init__.py +1 -0
  122. agent_assure/policies/base.py +170 -0
  123. agent_assure/policies/catalog.py +58 -0
  124. agent_assure/policies/evidence.py +57 -0
  125. agent_assure/policies/human_review.py +25 -0
  126. agent_assure/policies/injection.py +30 -0
  127. agent_assure/policies/output_schema.py +27 -0
  128. agent_assure/policies/privacy.py +65 -0
  129. agent_assure/policies/providers.py +60 -0
  130. agent_assure/policies/review_boundary.py +10 -0
  131. agent_assure/policies/runtime.py +22 -0
  132. agent_assure/policies/tools.py +45 -0
  133. agent_assure/privacy/__init__.py +18 -0
  134. agent_assure/privacy/detectors.py +36 -0
  135. agent_assure/privacy/redaction.py +121 -0
  136. agent_assure/privacy/safe_errors.py +51 -0
  137. agent_assure/release_evidence.py +487 -0
  138. agent_assure/reporting/__init__.py +1 -0
  139. agent_assure/reporting/console.py +137 -0
  140. agent_assure/reporting/environment.py +191 -0
  141. agent_assure/reporting/evidence_diff_html.py +1035 -0
  142. agent_assure/reporting/json_report.py +73 -0
  143. agent_assure/reporting/live.py +386 -0
  144. agent_assure/reporting/markdown.py +227 -0
  145. agent_assure/reporting/packet.py +186 -0
  146. agent_assure/reporting/sbom.py +126 -0
  147. agent_assure/runner/__init__.py +35 -0
  148. agent_assure/runner/clock.py +17 -0
  149. agent_assure/runner/emergency.py +1 -0
  150. agent_assure/runner/evidence.py +107 -0
  151. agent_assure/runner/fixture_runner.py +247 -0
  152. agent_assure/runner/fixture_values.py +20 -0
  153. agent_assure/runner/governance_controls.py +109 -0
  154. agent_assure/runner/ids.py +20 -0
  155. agent_assure/runner/records.py +91 -0
  156. agent_assure/runner/registry.py +54 -0
  157. agent_assure/runner/subprocess_harness.py +287 -0
  158. agent_assure/schema/__init__.py +98 -0
  159. agent_assure/schema/base.py +38 -0
  160. agent_assure/schema/common.py +105 -0
  161. agent_assure/schema/comparison.py +47 -0
  162. agent_assure/schema/environment.py +32 -0
  163. agent_assure/schema/evaluation.py +48 -0
  164. agent_assure/schema/expectation.py +66 -0
  165. agent_assure/schema/export.py +83 -0
  166. agent_assure/schema/live.py +1310 -0
  167. agent_assure/schema/packet.py +37 -0
  168. agent_assure/schema/provenance.py +18 -0
  169. agent_assure/schema/release.py +61 -0
  170. agent_assure/schema/run.py +237 -0
  171. agent_assure/schema/runtime.py +46 -0
  172. agent_assure/schema/suite.py +128 -0
  173. agent_assure/schema/telemetry.py +50 -0
  174. agent_assure/schema/validation.py +29 -0
  175. agent_assure/schema_resources/__init__.py +1 -0
  176. agent_assure/schema_resources/v0.1.0/agent-run-record.schema.json +819 -0
  177. agent_assure/schema_resources/v0.1.0/comparison-report.schema.json +753 -0
  178. agent_assure/schema_resources/v0.1.0/comparison-summary.schema.json +245 -0
  179. agent_assure/schema_resources/v0.1.0/compiled-suite.schema.json +323 -0
  180. agent_assure/schema_resources/v0.1.0/environment-info.schema.json +148 -0
  181. agent_assure/schema_resources/v0.1.0/evaluation-report.schema.json +476 -0
  182. agent_assure/schema_resources/v0.1.0/evaluation-summary.schema.json +286 -0
  183. agent_assure/schema_resources/v0.1.0/evidence-packet.schema.json +581 -0
  184. agent_assure/schema_resources/v0.1.0/expectation-change-record.schema.json +97 -0
  185. agent_assure/schema_resources/v0.1.0/expectation.schema.json +129 -0
  186. agent_assure/schema_resources/v0.1.0/fixture-manifest.schema.json +92 -0
  187. agent_assure/schema_resources/v0.1.0/live-comparison-report.schema.json +328 -0
  188. agent_assure/schema_resources/v0.1.0/live-evaluation-report.schema.json +860 -0
  189. agent_assure/schema_resources/v0.1.0/live-protocol-record.schema.json +353 -0
  190. agent_assure/schema_resources/v0.1.0/release-artifact-manifest.schema.json +233 -0
  191. agent_assure/schema_resources/v0.1.0/release-digest-replay.schema.json +113 -0
  192. agent_assure/schema_resources/v0.1.0/run-set.schema.json +921 -0
  193. agent_assure/schema_resources/v0.1.0/span-plan.schema.json +131 -0
  194. agent_assure/schema_resources/v0.2.0/agent-run-record.schema.json +862 -0
  195. agent_assure/schema_resources/v0.2.0/comparison-report.schema.json +753 -0
  196. agent_assure/schema_resources/v0.2.0/comparison-summary.schema.json +245 -0
  197. agent_assure/schema_resources/v0.2.0/compiled-suite.schema.json +323 -0
  198. agent_assure/schema_resources/v0.2.0/emergency-process-record.schema.json +260 -0
  199. agent_assure/schema_resources/v0.2.0/environment-info.schema.json +148 -0
  200. agent_assure/schema_resources/v0.2.0/evaluation-report.schema.json +476 -0
  201. agent_assure/schema_resources/v0.2.0/evaluation-summary.schema.json +286 -0
  202. agent_assure/schema_resources/v0.2.0/evidence-packet.schema.json +581 -0
  203. agent_assure/schema_resources/v0.2.0/expectation-change-record.schema.json +97 -0
  204. agent_assure/schema_resources/v0.2.0/expectation.schema.json +129 -0
  205. agent_assure/schema_resources/v0.2.0/fixture-manifest.schema.json +92 -0
  206. agent_assure/schema_resources/v0.2.0/live-comparison-report.schema.json +496 -0
  207. agent_assure/schema_resources/v0.2.0/live-drift-report.schema.json +1011 -0
  208. agent_assure/schema_resources/v0.2.0/live-evaluation-report.schema.json +1361 -0
  209. agent_assure/schema_resources/v0.2.0/live-protocol-record.schema.json +1097 -0
  210. agent_assure/schema_resources/v0.2.0/live-trajectory-report.schema.json +770 -0
  211. agent_assure/schema_resources/v0.2.0/release-artifact-manifest.schema.json +233 -0
  212. agent_assure/schema_resources/v0.2.0/release-digest-replay.schema.json +113 -0
  213. agent_assure/schema_resources/v0.2.0/run-set.schema.json +1230 -0
  214. agent_assure/schema_resources/v0.2.0/span-plan.schema.json +156 -0
  215. agent_assure/schema_resources/v0.3.0/agent-run-record.schema.json +862 -0
  216. agent_assure/schema_resources/v0.3.0/comparison-report.schema.json +753 -0
  217. agent_assure/schema_resources/v0.3.0/comparison-summary.schema.json +245 -0
  218. agent_assure/schema_resources/v0.3.0/compiled-suite.schema.json +323 -0
  219. agent_assure/schema_resources/v0.3.0/emergency-process-record.schema.json +260 -0
  220. agent_assure/schema_resources/v0.3.0/environment-info.schema.json +148 -0
  221. agent_assure/schema_resources/v0.3.0/evaluation-report.schema.json +476 -0
  222. agent_assure/schema_resources/v0.3.0/evaluation-summary.schema.json +286 -0
  223. agent_assure/schema_resources/v0.3.0/evidence-packet.schema.json +581 -0
  224. agent_assure/schema_resources/v0.3.0/expectation-change-record.schema.json +97 -0
  225. agent_assure/schema_resources/v0.3.0/expectation.schema.json +129 -0
  226. agent_assure/schema_resources/v0.3.0/fixture-manifest.schema.json +92 -0
  227. agent_assure/schema_resources/v0.3.0/live-comparison-report.schema.json +496 -0
  228. agent_assure/schema_resources/v0.3.0/live-drift-report.schema.json +1011 -0
  229. agent_assure/schema_resources/v0.3.0/live-evaluation-report.schema.json +1361 -0
  230. agent_assure/schema_resources/v0.3.0/live-protocol-record.schema.json +1097 -0
  231. agent_assure/schema_resources/v0.3.0/live-trajectory-report.schema.json +770 -0
  232. agent_assure/schema_resources/v0.3.0/release-artifact-manifest.schema.json +233 -0
  233. agent_assure/schema_resources/v0.3.0/release-digest-replay.schema.json +113 -0
  234. agent_assure/schema_resources/v0.3.0/run-set.schema.json +1230 -0
  235. agent_assure/schema_resources/v0.3.0/span-plan.schema.json +156 -0
  236. agent_assure/telemetry/__init__.py +15 -0
  237. agent_assure/telemetry/context.py +55 -0
  238. agent_assure/telemetry/otel_mapping.py +78 -0
  239. agent_assure/telemetry/otel_sdk.py +138 -0
  240. agent_assure/telemetry/privacy_filter.py +7 -0
  241. agent_assure/telemetry/semconv_lock.py +4 -0
  242. agent_assure/telemetry/span_plan.py +3 -0
  243. agent_assure-0.3.0.dist-info/METADATA +376 -0
  244. agent_assure-0.3.0.dist-info/RECORD +247 -0
  245. agent_assure-0.3.0.dist-info/WHEEL +4 -0
  246. agent_assure-0.3.0.dist-info/entry_points.txt +2 -0
  247. agent_assure-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,4 @@
1
+ """agent-assure public package."""
2
+
3
+ __version__ = "0.3.0"
4
+ SCHEMA_VERSION = "0.2.0"
@@ -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,4 @@
1
+ from agent_assure.authoring.compiler import compile_suite
2
+ from agent_assure.authoring.yaml_lint import lint_yaml
3
+
4
+ __all__ = ["compile_suite", "lint_yaml"]
@@ -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,9 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from agent_assure.authoring.yaml_nodes import YamlWarning, load_yaml_nodes
6
+
7
+
8
+ def lint_yaml(path: Path) -> tuple[YamlWarning, ...]:
9
+ return load_yaml_nodes(path).warnings
@@ -0,0 +1,4 @@
1
+
2
+ from agent_assure.authoring.yaml_nodes import LoadedYaml, load_yaml_nodes
3
+
4
+ __all__ = ["LoadedYaml", "load_yaml_nodes"]
@@ -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,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import rfc8785
6
+
7
+
8
+ def canonical_bytes(projected: Any) -> bytes:
9
+ result = rfc8785.dumps(projected)
10
+ if isinstance(result, bytes):
11
+ return result
12
+ return result.encode("utf-8")
@@ -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__}")
@@ -0,0 +1,3 @@
1
+ from agent_assure.canonical.normalize import digest_projection
2
+
3
+ __all__ = ["digest_projection"]