agent-lifecycle-kit 1.50.1__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_lifecycle/__init__.py +5 -0
- agent_lifecycle/__main__.py +5 -0
- agent_lifecycle/_version.py +3 -0
- agent_lifecycle/adapter_sessions/__init__.py +41 -0
- agent_lifecycle/adapter_sessions/contracts.py +133 -0
- agent_lifecycle/adapter_sessions/env.py +44 -0
- agent_lifecycle/adapter_sessions/launcher.py +101 -0
- agent_lifecycle/adapter_sessions/process.py +39 -0
- agent_lifecycle/adapter_sessions/redaction.py +30 -0
- agent_lifecycle/adapter_sessions/session_store.py +64 -0
- agent_lifecycle/adapter_sessions/task_intake.py +450 -0
- agent_lifecycle/adapter_sessions/workflow_bridge.py +154 -0
- agent_lifecycle/audit/__init__.py +67 -0
- agent_lifecycle/audit/bug_forensics.py +94 -0
- agent_lifecycle/audit/implementation.py +516 -0
- agent_lifecycle/audit/ownership.py +119 -0
- agent_lifecycle/audit/proof_integrity.py +814 -0
- agent_lifecycle/audit/review_verdict.py +116 -0
- agent_lifecycle/changesets/__init__.py +5 -0
- agent_lifecycle/changesets/git.py +35 -0
- agent_lifecycle/cli/__init__.py +5 -0
- agent_lifecycle/cli/adapter.py +360 -0
- agent_lifecycle/cli/dispatch.py +1220 -0
- agent_lifecycle/cli/followup.py +89 -0
- agent_lifecycle/cli/main.py +41 -0
- agent_lifecycle/cli/metrics_parser.py +49 -0
- agent_lifecycle/cli/parsers.py +618 -0
- agent_lifecycle/cli/policy.py +101 -0
- agent_lifecycle/cli/progress_hooks.py +117 -0
- agent_lifecycle/cli/worktree.py +74 -0
- agent_lifecycle/compiler/__init__.py +17 -0
- agent_lifecycle/compiler/small_model_packets.py +322 -0
- agent_lifecycle/compiler/task_packets.py +229 -0
- agent_lifecycle/context/__init__.py +20 -0
- agent_lifecycle/context/episode_retrieval.py +30 -0
- agent_lifecycle/context/external_memory.py +71 -0
- agent_lifecycle/context/profiles.py +104 -0
- agent_lifecycle/context/rendering.py +226 -0
- agent_lifecycle/contracts/__init__.py +25 -0
- agent_lifecycle/contracts/adapter_contract_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_event_schemas.py +118 -0
- agent_lifecycle/contracts/adapter_session_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_task_schemas.py +90 -0
- agent_lifecycle/contracts/audit_schemas.py +133 -0
- agent_lifecycle/contracts/bug_forensics_schemas.py +400 -0
- agent_lifecycle/contracts/canonical.py +55 -0
- agent_lifecycle/contracts/compatibility.py +242 -0
- agent_lifecycle/contracts/context_model_schemas.py +265 -0
- agent_lifecycle/contracts/core_schemas.py +252 -0
- agent_lifecycle/contracts/cross_check_schemas.py +121 -0
- agent_lifecycle/contracts/errors.py +26 -0
- agent_lifecycle/contracts/evidence_import_schemas.py +152 -0
- agent_lifecycle/contracts/host_capability_schemas.py +64 -0
- agent_lifecycle/contracts/import_dialect_schemas.py +166 -0
- agent_lifecycle/contracts/metric_schemas.py +315 -0
- agent_lifecycle/contracts/paths.py +31 -0
- agent_lifecycle/contracts/plan_contract_schemas.py +148 -0
- agent_lifecycle/contracts/policy_schemas.py +350 -0
- agent_lifecycle/contracts/progress_bridge_schemas.py +114 -0
- agent_lifecycle/contracts/progress_hook_schemas.py +119 -0
- agent_lifecycle/contracts/proof_integrity_schemas.py +240 -0
- agent_lifecycle/contracts/release_contract_schemas.py +476 -0
- agent_lifecycle/contracts/review_mesh_recommendation_schemas.py +64 -0
- agent_lifecycle/contracts/review_mesh_schemas.py +222 -0
- agent_lifecycle/contracts/review_quality_schemas.py +109 -0
- agent_lifecycle/contracts/runner_schemas.py +91 -0
- agent_lifecycle/contracts/runner_worktree_schemas.py +389 -0
- agent_lifecycle/contracts/sandbox_schemas.py +159 -0
- agent_lifecycle/contracts/schema_builders.py +23 -0
- agent_lifecycle/contracts/schemas.py +93 -0
- agent_lifecycle/contracts/status_goal_schemas.py +469 -0
- agent_lifecycle/contracts/task_template_schemas.py +96 -0
- agent_lifecycle/contracts/usage_export_schemas.py +68 -0
- agent_lifecycle/diagnostics/__init__.py +17 -0
- agent_lifecycle/diagnostics/bundles.py +124 -0
- agent_lifecycle/diagnostics/readiness.py +733 -0
- agent_lifecycle/evidence_index/__init__.py +39 -0
- agent_lifecycle/evidence_index/core.py +272 -0
- agent_lifecycle/evidence_index/episode_index.py +329 -0
- agent_lifecycle/evidence_index/external_context.py +276 -0
- agent_lifecycle/followup/__init__.py +23 -0
- agent_lifecycle/followup/records.py +404 -0
- agent_lifecycle/freeze/__init__.py +5 -0
- agent_lifecycle/freeze/locks.py +23 -0
- agent_lifecycle/goal/__init__.py +15 -0
- agent_lifecycle/goal/records.py +244 -0
- agent_lifecycle/goal/view.py +109 -0
- agent_lifecycle/host_protocol/__init__.py +65 -0
- agent_lifecycle/host_protocol/acp_capability.py +195 -0
- agent_lifecycle/host_protocol/capabilities.py +268 -0
- agent_lifecycle/host_protocol/contracts.py +214 -0
- agent_lifecycle/host_protocol/event_capture.py +390 -0
- agent_lifecycle/host_protocol/events.py +196 -0
- agent_lifecycle/host_protocol/inspection.py +194 -0
- agent_lifecycle/host_protocol/inspection_common.py +310 -0
- agent_lifecycle/host_protocol/inspection_cursor.py +164 -0
- agent_lifecycle/host_protocol/inspection_gemini_cli.py +163 -0
- agent_lifecycle/host_protocol/inspection_hermes.py +161 -0
- agent_lifecycle/host_protocol/inspection_kimi_code.py +169 -0
- agent_lifecycle/host_protocol/inspection_opencode.py +147 -0
- agent_lifecycle/host_protocol/inspection_qwen_code.py +143 -0
- agent_lifecycle/host_protocol/receipts.py +58 -0
- agent_lifecycle/host_protocol/scaffold.py +329 -0
- agent_lifecycle/host_protocol/validation.py +229 -0
- agent_lifecycle/imports/__init__.py +76 -0
- agent_lifecycle/imports/agentskills_profile.py +54 -0
- agent_lifecycle/imports/bmad_profile.py +49 -0
- agent_lifecycle/imports/constitution_adr.py +91 -0
- agent_lifecycle/imports/dialect_profiles.py +121 -0
- agent_lifecycle/imports/external_agent.py +28 -0
- agent_lifecycle/imports/external_dialects.py +486 -0
- agent_lifecycle/imports/external_workflow.py +28 -0
- agent_lifecycle/imports/multi_markdown.py +111 -0
- agent_lifecycle/imports/openspec_profile.py +49 -0
- agent_lifecycle/imports/planning.py +536 -0
- agent_lifecycle/imports/spec_kit_profile.py +49 -0
- agent_lifecycle/imports/spec_kitty_profile.py +49 -0
- agent_lifecycle/metrics/__init__.py +60 -0
- agent_lifecycle/metrics/cost_collection.py +315 -0
- agent_lifecycle/metrics/costs.py +203 -0
- agent_lifecycle/metrics/outcome_index.py +330 -0
- agent_lifecycle/metrics/phase_resources.py +208 -0
- agent_lifecycle/metrics/recommendations.py +507 -0
- agent_lifecycle/metrics/regression_signals.py +64 -0
- agent_lifecycle/metrics/usage_export.py +390 -0
- agent_lifecycle/model_routing/__init__.py +19 -0
- agent_lifecycle/model_routing/profiles.py +323 -0
- agent_lifecycle/model_routing/receipts.py +140 -0
- agent_lifecycle/model_routing/resolver.py +566 -0
- agent_lifecycle/neutrality/__init__.py +12 -0
- agent_lifecycle/neutrality/__main__.py +6 -0
- agent_lifecycle/neutrality/authority.py +265 -0
- agent_lifecycle/neutrality/canonical.py +28 -0
- agent_lifecycle/neutrality/cli.py +294 -0
- agent_lifecycle/neutrality/ed25519.py +124 -0
- agent_lifecycle/neutrality/errors.py +89 -0
- agent_lifecycle/neutrality/gate.py +269 -0
- agent_lifecycle/neutrality/paths.py +92 -0
- agent_lifecycle/neutrality/policy.py +76 -0
- agent_lifecycle/neutrality/receipt.py +135 -0
- agent_lifecycle/neutrality/scanner.py +310 -0
- agent_lifecycle/neutrality/test_runner.py +116 -0
- agent_lifecycle/planning/__init__.py +47 -0
- agent_lifecycle/planning/acceptance_markdown.py +159 -0
- agent_lifecycle/planning/completeness.py +362 -0
- agent_lifecycle/planning/continuity.py +255 -0
- agent_lifecycle/planning/templates.py +294 -0
- agent_lifecycle/planning/tiering.py +87 -0
- agent_lifecycle/planning/validation.py +72 -0
- agent_lifecycle/policy/__init__.py +37 -0
- agent_lifecycle/policy/adaptive_lifecycle.py +416 -0
- agent_lifecycle/policy/apply.py +44 -0
- agent_lifecycle/policy/proposals.py +189 -0
- agent_lifecycle/policy/quality_floor.py +144 -0
- agent_lifecycle/policy/runtime_receipts.py +160 -0
- agent_lifecycle/py.typed +1 -0
- agent_lifecycle/quality/__init__.py +89 -0
- agent_lifecycle/quality/bug_forensics.py +727 -0
- agent_lifecycle/quality/bug_forensics_advisor.py +88 -0
- agent_lifecycle/quality/bug_forensics_recipes.py +239 -0
- agent_lifecycle/quality/cross_check.py +437 -0
- agent_lifecycle/quality/failure_classification.py +318 -0
- agent_lifecycle/quality/packs.py +265 -0
- agent_lifecycle/reporting/__init__.py +37 -0
- agent_lifecycle/reporting/change_summary.py +156 -0
- agent_lifecycle/reporting/event_feed.py +101 -0
- agent_lifecycle/reporting/progress_bridge.py +153 -0
- agent_lifecycle/reporting/progress_hooks.py +161 -0
- agent_lifecycle/reporting/progress_terminal.py +95 -0
- agent_lifecycle/reporting/progress_view.py +284 -0
- agent_lifecycle/reporting/status_view.py +125 -0
- agent_lifecycle/reporting/usage_export.py +59 -0
- agent_lifecycle/review/__init__.py +5 -0
- agent_lifecycle/review/validation.py +29 -0
- agent_lifecycle/review_mesh/__init__.py +83 -0
- agent_lifecycle/review_mesh/assignments.py +160 -0
- agent_lifecycle/review_mesh/contracts.py +685 -0
- agent_lifecycle/review_mesh/operator_templates.py +320 -0
- agent_lifecycle/review_mesh/quorum.py +38 -0
- agent_lifecycle/review_mesh/recommendation.py +487 -0
- agent_lifecycle/review_mesh/results.py +98 -0
- agent_lifecycle/review_mesh/synthesis.py +102 -0
- agent_lifecycle/runner/__init__.py +47 -0
- agent_lifecycle/runner/attempt_snapshots.py +244 -0
- agent_lifecycle/runner/core.py +653 -0
- agent_lifecycle/runner/sandbox_receipts.py +628 -0
- agent_lifecycle/specification/__init__.py +23 -0
- agent_lifecycle/specification/completion_check.py +135 -0
- agent_lifecycle/specification/completion_gate.py +501 -0
- agent_lifecycle/specification/completion_signal.py +75 -0
- agent_lifecycle/specification/validation.py +48 -0
- agent_lifecycle/workflow/__init__.py +79 -0
- agent_lifecycle/workflow/artifacts.py +74 -0
- agent_lifecycle/workflow/budget_decisions.py +370 -0
- agent_lifecycle/workflow/budget_policy.py +140 -0
- agent_lifecycle/workflow/budget_receipts.py +70 -0
- agent_lifecycle/workflow/bug_forensics_gates.py +284 -0
- agent_lifecycle/workflow/controller.py +45 -0
- agent_lifecycle/workflow/events.py +50 -0
- agent_lifecycle/workflow/final_proof_integrity.py +74 -0
- agent_lifecycle/workflow/finalization.py +422 -0
- agent_lifecycle/workflow/gates.py +185 -0
- agent_lifecycle/workflow/implementation_audit_gate.py +126 -0
- agent_lifecycle/workflow/leases.py +220 -0
- agent_lifecycle/workflow/lineage.py +108 -0
- agent_lifecycle/workflow/managed_runner.py +167 -0
- agent_lifecycle/workflow/model_usage.py +106 -0
- agent_lifecycle/workflow/next_action.py +86 -0
- agent_lifecycle/workflow/operation_kernel.py +98 -0
- agent_lifecycle/workflow/plan_adoption.py +439 -0
- agent_lifecycle/workflow/query.py +52 -0
- agent_lifecycle/workflow/review_mesh_gate.py +93 -0
- agent_lifecycle/workflow/reviews.py +168 -0
- agent_lifecycle/workflow/run_transitions.py +163 -0
- agent_lifecycle/workflow/sandbox_policy.py +214 -0
- agent_lifecycle/workflow/selectors.py +57 -0
- agent_lifecycle/workflow/state.py +130 -0
- agent_lifecycle/workflow/task_transitions.py +421 -0
- agent_lifecycle/worktree/__init__.py +17 -0
- agent_lifecycle/worktree/isolation.py +407 -0
- agent_lifecycle_kit-1.50.1.dist-info/METADATA +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/RECORD +227 -0
- agent_lifecycle_kit-1.50.1.dist-info/WHEEL +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/entry_points.txt +3 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/LICENSE +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/NOTICE +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""Optional cross-check profile and receipt helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_lifecycle.contracts import LifecycleError, canonical_digest
|
|
8
|
+
|
|
9
|
+
CROSS_CHECK_PROFILE_SCHEMA = "agent-cross-check-profile.v1"
|
|
10
|
+
CROSS_CHECK_PROFILE_VALIDATION_SCHEMA = "agent-cross-check-profile-validation.v1"
|
|
11
|
+
CROSS_CHECK_RECEIPT_SCHEMA = "agent-cross-check-receipt.v1"
|
|
12
|
+
CROSS_CHECK_RECEIPT_VALIDATION_SCHEMA = "agent-cross-check-receipt-validation.v1"
|
|
13
|
+
|
|
14
|
+
RECEIPT_STATUSES = {"PASS", "FAIL", "SKIPPED"}
|
|
15
|
+
DEFAULT_RISK_TRIGGERS = ("S2", "security", "release", "high-risk", "bugfix", "regression")
|
|
16
|
+
RESOURCE_CAP_KEYS = {"maxInvocations", "maxInputTokens", "maxOutputTokens", "maxWallSeconds"}
|
|
17
|
+
USAGE_KEYS = {"invocations", "inputTokens", "outputTokens", "wallSeconds"}
|
|
18
|
+
MONEY_KEYS = {"costUsd", "cost_usd", "usd", "budgetUsd", "maxUsd", "money", "monetary"}
|
|
19
|
+
INDEPENDENCE_DIMENSIONS = {"host", "model"}
|
|
20
|
+
INDEPENDENCE_IDENTITY_FIELDS = {"host": "hostIdentityHash", "model": "modelIdentityHash"}
|
|
21
|
+
INDEPENDENCE_STATUSES = {"INDEPENDENT", "NOT_REQUIRED", "NOT_PROVEN"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_cross_check_profile(
|
|
25
|
+
*,
|
|
26
|
+
profile_id: str = "optional-cross-check",
|
|
27
|
+
budget_cap: dict[str, int] | None = None,
|
|
28
|
+
risk_triggers: list[str] | None = None,
|
|
29
|
+
live_calls_allowed: bool = False,
|
|
30
|
+
independence_required: bool = False,
|
|
31
|
+
independence_dimensions: list[str] | None = None,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
"""Build the generic optional cross-check profile."""
|
|
34
|
+
|
|
35
|
+
cap = budget_cap or {"maxInvocations": 1, "maxInputTokens": 12000, "maxOutputTokens": 4000, "maxWallSeconds": 180}
|
|
36
|
+
body = {
|
|
37
|
+
"schemaVersion": CROSS_CHECK_PROFILE_SCHEMA,
|
|
38
|
+
"profileId": _required_string(profile_id, label="profileId", code="invalid-cross-check-profile"),
|
|
39
|
+
"status": "OPTIONAL",
|
|
40
|
+
"enabledByDefault": False,
|
|
41
|
+
"activationMode": "opt-in",
|
|
42
|
+
"advisoryByDefault": True,
|
|
43
|
+
"blockingRequiresPlanOptIn": True,
|
|
44
|
+
"requiresExplicitActivation": True,
|
|
45
|
+
"liveCallsAllowed": bool(live_calls_allowed),
|
|
46
|
+
"riskTriggers": _string_list(list(risk_triggers or DEFAULT_RISK_TRIGGERS), label="riskTriggers", code="invalid-cross-check-profile"),
|
|
47
|
+
"budgetCap": _resource_cap(cap, code="invalid-cross-check-profile"),
|
|
48
|
+
"budgetUnits": "tokens-and-resources",
|
|
49
|
+
"independencePolicy": _independence_policy(
|
|
50
|
+
required=independence_required,
|
|
51
|
+
dimensions=independence_dimensions,
|
|
52
|
+
),
|
|
53
|
+
"monetaryCostCanonical": False,
|
|
54
|
+
"productionPromotionClaimed": False,
|
|
55
|
+
}
|
|
56
|
+
return {**body, "profileDigest": canonical_digest(body)}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_cross_check_profile(profile: dict[str, Any]) -> dict[str, Any]:
|
|
60
|
+
blockers: list[dict[str, Any]] = []
|
|
61
|
+
if not isinstance(profile, dict):
|
|
62
|
+
raise LifecycleError("invalid-cross-check-profile", "cross-check profile must be an object")
|
|
63
|
+
if profile.get("schemaVersion") != CROSS_CHECK_PROFILE_SCHEMA:
|
|
64
|
+
blockers.append({"code": "cross-check-profile-schema-invalid"})
|
|
65
|
+
if not isinstance(profile.get("profileId"), str) or not profile["profileId"]:
|
|
66
|
+
blockers.append({"code": "cross-check-profile-id-missing"})
|
|
67
|
+
if profile.get("status") != "OPTIONAL":
|
|
68
|
+
blockers.append({"code": "cross-check-profile-not-optional"})
|
|
69
|
+
if profile.get("enabledByDefault") is not False:
|
|
70
|
+
blockers.append({"code": "cross-check-profile-default-enabled"})
|
|
71
|
+
if profile.get("activationMode") != "opt-in":
|
|
72
|
+
blockers.append({"code": "cross-check-profile-not-opt-in"})
|
|
73
|
+
if profile.get("advisoryByDefault") is not True:
|
|
74
|
+
blockers.append({"code": "cross-check-profile-not-advisory"})
|
|
75
|
+
if profile.get("blockingRequiresPlanOptIn") is not True:
|
|
76
|
+
blockers.append({"code": "cross-check-profile-blocking-not-plan-gated"})
|
|
77
|
+
if profile.get("requiresExplicitActivation") is not True:
|
|
78
|
+
blockers.append({"code": "cross-check-profile-activation-not-explicit"})
|
|
79
|
+
if not isinstance(profile.get("liveCallsAllowed"), bool):
|
|
80
|
+
blockers.append({"code": "cross-check-profile-live-calls-invalid"})
|
|
81
|
+
_validate_string_list(profile.get("riskTriggers"), "cross-check-profile-risk-triggers-invalid", blockers)
|
|
82
|
+
_validate_resource_cap(profile.get("budgetCap"), blockers)
|
|
83
|
+
if profile.get("budgetUnits") != "tokens-and-resources":
|
|
84
|
+
blockers.append({"code": "cross-check-profile-budget-units-invalid"})
|
|
85
|
+
_validate_independence_policy(profile.get("independencePolicy"), blockers)
|
|
86
|
+
if profile.get("monetaryCostCanonical") is not False:
|
|
87
|
+
blockers.append({"code": "cross-check-profile-money-canonical"})
|
|
88
|
+
if profile.get("productionPromotionClaimed") is not False:
|
|
89
|
+
blockers.append({"code": "cross-check-profile-production-claim"})
|
|
90
|
+
expected_digest = canonical_digest({key: value for key, value in profile.items() if key != "profileDigest"})
|
|
91
|
+
if profile.get("profileDigest") != expected_digest:
|
|
92
|
+
blockers.append({"code": "cross-check-profile-digest-mismatch"})
|
|
93
|
+
body = {
|
|
94
|
+
"schemaVersion": CROSS_CHECK_PROFILE_VALIDATION_SCHEMA,
|
|
95
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
96
|
+
"profileId": profile.get("profileId") if isinstance(profile.get("profileId"), str) else None,
|
|
97
|
+
"enabledByDefault": profile.get("enabledByDefault") if isinstance(profile.get("enabledByDefault"), bool) else None,
|
|
98
|
+
"blockers": blockers,
|
|
99
|
+
"profileDigest": profile.get("profileDigest"),
|
|
100
|
+
}
|
|
101
|
+
return {**body, "validationDigest": canonical_digest(body)}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_cross_check_receipt(
|
|
105
|
+
*,
|
|
106
|
+
profile: dict[str, Any],
|
|
107
|
+
subject: dict[str, Any],
|
|
108
|
+
reviewer: dict[str, Any],
|
|
109
|
+
budget_usage: dict[str, int],
|
|
110
|
+
findings: list[dict[str, Any]] | None = None,
|
|
111
|
+
blocking: bool = False,
|
|
112
|
+
live_calls_started: bool = False,
|
|
113
|
+
evidence_ids: list[str] | None = None,
|
|
114
|
+
status: str | None = None,
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
"""Build an optional cross-check receipt with a hard resource cap."""
|
|
117
|
+
|
|
118
|
+
profile_validation = validate_cross_check_profile(profile)
|
|
119
|
+
usage = _budget_usage(budget_usage, code="invalid-cross-check-receipt")
|
|
120
|
+
cap = profile.get("budgetCap") if isinstance(profile, dict) and isinstance(profile.get("budgetCap"), dict) else {}
|
|
121
|
+
cap_blockers = _budget_blockers(usage, cap)
|
|
122
|
+
if live_calls_started and profile.get("liveCallsAllowed") is not True:
|
|
123
|
+
cap_blockers.append({"code": "cross-check-live-call-not-allowed"})
|
|
124
|
+
if blocking and subject.get("blockingCrossCheckRequired") is not True:
|
|
125
|
+
cap_blockers.append({"code": "cross-check-blocking-without-plan-opt-in"})
|
|
126
|
+
if profile_validation["status"] != "PASS":
|
|
127
|
+
cap_blockers.append({"code": "cross-check-profile-invalid"})
|
|
128
|
+
independence = _independence_evidence(subject, reviewer, profile.get("independencePolicy") if isinstance(profile, dict) else None)
|
|
129
|
+
if independence["required"] and independence["status"] != "INDEPENDENT":
|
|
130
|
+
cap_blockers.append({"code": "cross-check-independence-not-proven"})
|
|
131
|
+
body = {
|
|
132
|
+
"schemaVersion": CROSS_CHECK_RECEIPT_SCHEMA,
|
|
133
|
+
"status": _enum(status or ("FAIL" if cap_blockers else "PASS"), RECEIPT_STATUSES, label="status", code="invalid-cross-check-receipt"),
|
|
134
|
+
"profileId": profile.get("profileId"),
|
|
135
|
+
"profileDigest": profile.get("profileDigest"),
|
|
136
|
+
"subject": dict(subject),
|
|
137
|
+
"reviewer": dict(reviewer),
|
|
138
|
+
"findings": list(findings or []),
|
|
139
|
+
"blocking": bool(blocking),
|
|
140
|
+
"advisory": not blocking,
|
|
141
|
+
"independence": independence,
|
|
142
|
+
"budgetCap": dict(cap),
|
|
143
|
+
"budgetUsage": usage,
|
|
144
|
+
"liveCallsStarted": bool(live_calls_started),
|
|
145
|
+
"evidenceIds": _string_list(evidence_ids or [], label="evidenceIds", code="invalid-cross-check-receipt", allow_empty=True),
|
|
146
|
+
"blockers": cap_blockers,
|
|
147
|
+
"productionPromotionClaimed": False,
|
|
148
|
+
}
|
|
149
|
+
return {**body, "receiptDigest": canonical_digest(body)}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def validate_cross_check_receipt(receipt: dict[str, Any], *, profile: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
153
|
+
blockers: list[dict[str, Any]] = []
|
|
154
|
+
if not isinstance(receipt, dict):
|
|
155
|
+
raise LifecycleError("invalid-cross-check-receipt", "cross-check receipt must be an object")
|
|
156
|
+
if receipt.get("schemaVersion") != CROSS_CHECK_RECEIPT_SCHEMA:
|
|
157
|
+
blockers.append({"code": "cross-check-receipt-schema-invalid"})
|
|
158
|
+
status = receipt.get("status")
|
|
159
|
+
if status not in RECEIPT_STATUSES:
|
|
160
|
+
blockers.append({"code": "cross-check-receipt-status-invalid", "status": status})
|
|
161
|
+
if not isinstance(receipt.get("profileId"), str) or not receipt["profileId"]:
|
|
162
|
+
blockers.append({"code": "cross-check-receipt-profile-id-missing"})
|
|
163
|
+
_check_digest(receipt.get("profileDigest"), "cross-check-receipt-profile-digest-invalid", blockers)
|
|
164
|
+
if profile is not None:
|
|
165
|
+
profile_validation = validate_cross_check_profile(profile)
|
|
166
|
+
if profile_validation["status"] != "PASS":
|
|
167
|
+
blockers.append({"code": "cross-check-profile-invalid"})
|
|
168
|
+
if receipt.get("profileDigest") != profile.get("profileDigest"):
|
|
169
|
+
blockers.append({"code": "cross-check-profile-digest-mismatch"})
|
|
170
|
+
if receipt.get("liveCallsStarted") is True and profile.get("liveCallsAllowed") is not True:
|
|
171
|
+
blockers.append({"code": "cross-check-live-call-not-allowed"})
|
|
172
|
+
subject = receipt.get("subject")
|
|
173
|
+
if not isinstance(subject, dict) or not subject:
|
|
174
|
+
blockers.append({"code": "cross-check-subject-invalid"})
|
|
175
|
+
subject = {}
|
|
176
|
+
if receipt.get("blocking") is True and subject.get("blockingCrossCheckRequired") is not True:
|
|
177
|
+
blockers.append({"code": "cross-check-blocking-without-plan-opt-in"})
|
|
178
|
+
if receipt.get("advisory") is not (receipt.get("blocking") is not True):
|
|
179
|
+
blockers.append({"code": "cross-check-advisory-mismatch"})
|
|
180
|
+
reviewer = receipt.get("reviewer")
|
|
181
|
+
if not isinstance(reviewer, dict) or not reviewer:
|
|
182
|
+
blockers.append({"code": "cross-check-reviewer-invalid"})
|
|
183
|
+
reviewer = {}
|
|
184
|
+
independence = receipt.get("independence")
|
|
185
|
+
_validate_independence_evidence(independence, blockers)
|
|
186
|
+
if isinstance(independence, dict) and independence.get("required") is True and independence.get("status") != "INDEPENDENT":
|
|
187
|
+
blockers.append({"code": "cross-check-independence-not-proven"})
|
|
188
|
+
if profile is not None and isinstance(subject, dict) and isinstance(reviewer, dict):
|
|
189
|
+
expected_independence = _independence_evidence(subject, reviewer, profile.get("independencePolicy"))
|
|
190
|
+
if independence != expected_independence:
|
|
191
|
+
blockers.append({"code": "cross-check-independence-mismatch"})
|
|
192
|
+
if not isinstance(receipt.get("findings", []), list) or not all(isinstance(item, dict) for item in receipt.get("findings", [])):
|
|
193
|
+
blockers.append({"code": "cross-check-findings-invalid"})
|
|
194
|
+
cap = receipt.get("budgetCap")
|
|
195
|
+
usage = receipt.get("budgetUsage")
|
|
196
|
+
_validate_resource_cap(cap, blockers)
|
|
197
|
+
_validate_budget_usage(usage, blockers)
|
|
198
|
+
if isinstance(cap, dict) and isinstance(usage, dict):
|
|
199
|
+
blockers.extend(_budget_blockers(usage, cap))
|
|
200
|
+
if not isinstance(receipt.get("liveCallsStarted"), bool):
|
|
201
|
+
blockers.append({"code": "cross-check-live-calls-started-invalid"})
|
|
202
|
+
_check_string_list(receipt.get("evidenceIds", []), "cross-check-evidence-ids-invalid", blockers, allow_empty=True)
|
|
203
|
+
_check_object_list(receipt.get("blockers", []), "cross-check-blockers-invalid", blockers)
|
|
204
|
+
if receipt.get("productionPromotionClaimed") is not False:
|
|
205
|
+
blockers.append({"code": "cross-check-production-claim"})
|
|
206
|
+
expected_digest = canonical_digest({key: value for key, value in receipt.items() if key != "receiptDigest"})
|
|
207
|
+
if receipt.get("receiptDigest") != expected_digest:
|
|
208
|
+
blockers.append({"code": "cross-check-receipt-digest-mismatch"})
|
|
209
|
+
body = {
|
|
210
|
+
"schemaVersion": CROSS_CHECK_RECEIPT_VALIDATION_SCHEMA,
|
|
211
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
212
|
+
"receiptStatus": status if isinstance(status, str) else None,
|
|
213
|
+
"profileId": receipt.get("profileId") if isinstance(receipt.get("profileId"), str) else None,
|
|
214
|
+
"blocking": receipt.get("blocking") if isinstance(receipt.get("blocking"), bool) else None,
|
|
215
|
+
"independenceStatus": independence.get("status") if isinstance(independence, dict) else None,
|
|
216
|
+
"blockers": blockers,
|
|
217
|
+
"receiptDigest": receipt.get("receiptDigest"),
|
|
218
|
+
}
|
|
219
|
+
return {**body, "validationDigest": canonical_digest(body)}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def require_cross_check_profile_pass(validation: dict[str, Any]) -> dict[str, Any]:
|
|
223
|
+
if validation.get("status") != "PASS":
|
|
224
|
+
raise LifecycleError("cross-check-profile-validation-failed", "cross-check profile validation failed", {"validation": validation})
|
|
225
|
+
return validation
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def require_cross_check_receipt_pass(validation: dict[str, Any]) -> dict[str, Any]:
|
|
229
|
+
if validation.get("status") != "PASS" or validation.get("receiptStatus") != "PASS":
|
|
230
|
+
raise LifecycleError("cross-check-receipt-validation-failed", "cross-check receipt did not pass", {"validation": validation})
|
|
231
|
+
return validation
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _budget_blockers(usage: dict[str, Any], cap: dict[str, Any]) -> list[dict[str, Any]]:
|
|
235
|
+
blockers: list[dict[str, Any]] = []
|
|
236
|
+
cap_map = {
|
|
237
|
+
"invocations": cap.get("maxInvocations"),
|
|
238
|
+
"inputTokens": cap.get("maxInputTokens"),
|
|
239
|
+
"outputTokens": cap.get("maxOutputTokens"),
|
|
240
|
+
"wallSeconds": cap.get("maxWallSeconds"),
|
|
241
|
+
}
|
|
242
|
+
for key, value in usage.items():
|
|
243
|
+
limit = cap_map.get(key)
|
|
244
|
+
if isinstance(limit, int) and value > limit:
|
|
245
|
+
blockers.append({"code": "cross-check-budget-cap-exceeded", "field": key, "limit": limit, "actual": value})
|
|
246
|
+
return blockers
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _resource_cap(value: dict[str, int], *, code: str) -> dict[str, int]:
|
|
250
|
+
if not isinstance(value, dict):
|
|
251
|
+
raise LifecycleError(code, "budgetCap must be an object")
|
|
252
|
+
result: dict[str, int] = {}
|
|
253
|
+
for key in value:
|
|
254
|
+
if key in MONEY_KEYS:
|
|
255
|
+
raise LifecycleError(code, "budgetCap must not use monetary limits")
|
|
256
|
+
if key not in RESOURCE_CAP_KEYS:
|
|
257
|
+
raise LifecycleError(code, "budgetCap field is unsupported", {"field": key})
|
|
258
|
+
item = value[key]
|
|
259
|
+
if not isinstance(item, int) or isinstance(item, bool) or item <= 0:
|
|
260
|
+
raise LifecycleError(code, "budgetCap values must be positive integers", {"field": key})
|
|
261
|
+
result[key] = item
|
|
262
|
+
if not result:
|
|
263
|
+
raise LifecycleError(code, "budgetCap must not be empty")
|
|
264
|
+
return dict(sorted(result.items()))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _budget_usage(value: dict[str, int], *, code: str) -> dict[str, int]:
|
|
268
|
+
if not isinstance(value, dict):
|
|
269
|
+
raise LifecycleError(code, "budgetUsage must be an object")
|
|
270
|
+
result: dict[str, int] = {}
|
|
271
|
+
for key in value:
|
|
272
|
+
if key in MONEY_KEYS:
|
|
273
|
+
raise LifecycleError(code, "budgetUsage must not use monetary fields")
|
|
274
|
+
if key not in USAGE_KEYS:
|
|
275
|
+
raise LifecycleError(code, "budgetUsage field is unsupported", {"field": key})
|
|
276
|
+
item = value[key]
|
|
277
|
+
if not isinstance(item, int) or isinstance(item, bool) or item < 0:
|
|
278
|
+
raise LifecycleError(code, "budgetUsage values must be non-negative integers", {"field": key})
|
|
279
|
+
result[key] = item
|
|
280
|
+
return {key: result.get(key, 0) for key in sorted(USAGE_KEYS)}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _independence_policy(*, required: bool, dimensions: list[str] | None) -> dict[str, Any]:
|
|
284
|
+
selected = dimensions or ["host", "model"]
|
|
285
|
+
for dimension in selected:
|
|
286
|
+
if dimension not in INDEPENDENCE_DIMENSIONS:
|
|
287
|
+
raise LifecycleError("invalid-cross-check-profile", "independence dimension is unsupported", {"dimension": dimension})
|
|
288
|
+
if len(selected) != len(set(selected)):
|
|
289
|
+
raise LifecycleError("invalid-cross-check-profile", "independence dimensions contain duplicates")
|
|
290
|
+
return {
|
|
291
|
+
"required": bool(required),
|
|
292
|
+
"requiredDimensions": list(selected),
|
|
293
|
+
"identityFields": dict(INDEPENDENCE_IDENTITY_FIELDS),
|
|
294
|
+
"providerNamesCanonical": False,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _independence_evidence(
|
|
299
|
+
subject: dict[str, Any],
|
|
300
|
+
reviewer: dict[str, Any],
|
|
301
|
+
policy: Any,
|
|
302
|
+
) -> dict[str, Any]:
|
|
303
|
+
selected_policy = policy if isinstance(policy, dict) else _independence_policy(required=False, dimensions=None)
|
|
304
|
+
required = selected_policy.get("required") is True
|
|
305
|
+
dimensions = [
|
|
306
|
+
item
|
|
307
|
+
for item in selected_policy.get("requiredDimensions", [])
|
|
308
|
+
if isinstance(item, str) and item in INDEPENDENCE_DIMENSIONS
|
|
309
|
+
] or ["host", "model"]
|
|
310
|
+
comparisons: list[dict[str, Any]] = []
|
|
311
|
+
independent = True
|
|
312
|
+
for dimension in dimensions:
|
|
313
|
+
field = INDEPENDENCE_IDENTITY_FIELDS[dimension]
|
|
314
|
+
subject_hash = subject.get(field)
|
|
315
|
+
reviewer_hash = reviewer.get(field)
|
|
316
|
+
proven = (
|
|
317
|
+
isinstance(subject_hash, str)
|
|
318
|
+
and len(subject_hash) == 64
|
|
319
|
+
and isinstance(reviewer_hash, str)
|
|
320
|
+
and len(reviewer_hash) == 64
|
|
321
|
+
and subject_hash != reviewer_hash
|
|
322
|
+
)
|
|
323
|
+
comparisons.append(
|
|
324
|
+
{
|
|
325
|
+
"dimension": dimension,
|
|
326
|
+
"identityField": field,
|
|
327
|
+
"subjectHash": subject_hash if isinstance(subject_hash, str) else None,
|
|
328
|
+
"reviewerHash": reviewer_hash if isinstance(reviewer_hash, str) else None,
|
|
329
|
+
"independent": proven,
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
independent = independent and proven
|
|
333
|
+
return {
|
|
334
|
+
"required": required,
|
|
335
|
+
"status": "INDEPENDENT" if required and independent else ("NOT_REQUIRED" if not required else "NOT_PROVEN"),
|
|
336
|
+
"requiredDimensions": dimensions,
|
|
337
|
+
"comparisons": comparisons,
|
|
338
|
+
"providerNamesCompared": False,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _validate_independence_policy(value: Any, blockers: list[dict[str, Any]]) -> None:
|
|
343
|
+
if not isinstance(value, dict):
|
|
344
|
+
blockers.append({"code": "cross-check-independence-policy-invalid"})
|
|
345
|
+
return
|
|
346
|
+
if not isinstance(value.get("required"), bool):
|
|
347
|
+
blockers.append({"code": "cross-check-independence-required-invalid"})
|
|
348
|
+
dimensions = value.get("requiredDimensions")
|
|
349
|
+
if not isinstance(dimensions, list) or not dimensions:
|
|
350
|
+
blockers.append({"code": "cross-check-independence-dimensions-invalid"})
|
|
351
|
+
elif len(dimensions) != len(set(dimensions)) or any(item not in INDEPENDENCE_DIMENSIONS for item in dimensions):
|
|
352
|
+
blockers.append({"code": "cross-check-independence-dimensions-invalid"})
|
|
353
|
+
identity_fields = value.get("identityFields")
|
|
354
|
+
if identity_fields != INDEPENDENCE_IDENTITY_FIELDS:
|
|
355
|
+
blockers.append({"code": "cross-check-independence-identity-fields-invalid"})
|
|
356
|
+
if value.get("providerNamesCanonical") is not False:
|
|
357
|
+
blockers.append({"code": "cross-check-independence-provider-names-canonical"})
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _validate_independence_evidence(value: Any, blockers: list[dict[str, Any]]) -> None:
|
|
361
|
+
if not isinstance(value, dict):
|
|
362
|
+
blockers.append({"code": "cross-check-independence-invalid"})
|
|
363
|
+
return
|
|
364
|
+
if not isinstance(value.get("required"), bool):
|
|
365
|
+
blockers.append({"code": "cross-check-independence-required-invalid"})
|
|
366
|
+
if value.get("status") not in INDEPENDENCE_STATUSES:
|
|
367
|
+
blockers.append({"code": "cross-check-independence-status-invalid"})
|
|
368
|
+
if not isinstance(value.get("requiredDimensions"), list) or not value["requiredDimensions"]:
|
|
369
|
+
blockers.append({"code": "cross-check-independence-dimensions-invalid"})
|
|
370
|
+
if not isinstance(value.get("comparisons"), list) or not all(isinstance(item, dict) for item in value["comparisons"]):
|
|
371
|
+
blockers.append({"code": "cross-check-independence-comparisons-invalid"})
|
|
372
|
+
if value.get("providerNamesCompared") is not False:
|
|
373
|
+
blockers.append({"code": "cross-check-independence-provider-name-compare"})
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _validate_resource_cap(value: Any, blockers: list[dict[str, Any]]) -> None:
|
|
377
|
+
if not isinstance(value, dict) or not value:
|
|
378
|
+
blockers.append({"code": "cross-check-budget-cap-invalid"})
|
|
379
|
+
return
|
|
380
|
+
for key, item in value.items():
|
|
381
|
+
if key in MONEY_KEYS:
|
|
382
|
+
blockers.append({"code": "cross-check-money-cap-not-allowed", "field": key})
|
|
383
|
+
elif key not in RESOURCE_CAP_KEYS:
|
|
384
|
+
blockers.append({"code": "cross-check-budget-cap-unsupported", "field": key})
|
|
385
|
+
elif not isinstance(item, int) or isinstance(item, bool) or item <= 0:
|
|
386
|
+
blockers.append({"code": "cross-check-budget-cap-value-invalid", "field": key})
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _validate_budget_usage(value: Any, blockers: list[dict[str, Any]]) -> None:
|
|
390
|
+
if not isinstance(value, dict):
|
|
391
|
+
blockers.append({"code": "cross-check-budget-usage-invalid"})
|
|
392
|
+
return
|
|
393
|
+
for key, item in value.items():
|
|
394
|
+
if key in MONEY_KEYS:
|
|
395
|
+
blockers.append({"code": "cross-check-money-usage-not-allowed", "field": key})
|
|
396
|
+
elif key not in USAGE_KEYS:
|
|
397
|
+
blockers.append({"code": "cross-check-budget-usage-unsupported", "field": key})
|
|
398
|
+
elif not isinstance(item, int) or isinstance(item, bool) or item < 0:
|
|
399
|
+
blockers.append({"code": "cross-check-budget-usage-value-invalid", "field": key})
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _required_string(value: Any, *, label: str, code: str) -> str:
|
|
403
|
+
if not isinstance(value, str) or not value:
|
|
404
|
+
raise LifecycleError(code, f"{label} is required")
|
|
405
|
+
return value
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _string_list(value: Any, *, label: str, code: str, allow_empty: bool = False) -> list[str]:
|
|
409
|
+
if not isinstance(value, list) or (not allow_empty and not value) or not all(isinstance(item, str) and item for item in value):
|
|
410
|
+
raise LifecycleError(code, f"{label} must be a list of strings")
|
|
411
|
+
return list(value)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _validate_string_list(value: Any, code: str, blockers: list[dict[str, Any]]) -> None:
|
|
415
|
+
if not isinstance(value, list) or not value or not all(isinstance(item, str) and item for item in value):
|
|
416
|
+
blockers.append({"code": code})
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _enum(value: Any, allowed: set[str], *, label: str, code: str) -> str:
|
|
420
|
+
if value not in allowed:
|
|
421
|
+
raise LifecycleError(code, f"{label} is unsupported", {label: value})
|
|
422
|
+
return str(value)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _check_digest(value: Any, code: str, blockers: list[dict[str, Any]]) -> None:
|
|
426
|
+
if not isinstance(value, str) or len(value) != 64:
|
|
427
|
+
blockers.append({"code": code})
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _check_string_list(value: Any, code: str, blockers: list[dict[str, Any]], *, allow_empty: bool = False) -> None:
|
|
431
|
+
if not isinstance(value, list) or (not allow_empty and not value) or not all(isinstance(item, str) and item for item in value):
|
|
432
|
+
blockers.append({"code": code})
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _check_object_list(value: Any, code: str, blockers: list[dict[str, Any]]) -> None:
|
|
436
|
+
if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
|
|
437
|
+
blockers.append({"code": code})
|