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,72 @@
|
|
|
1
|
+
"""Validate portable plan manifest structure and write ownership."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_lifecycle.contracts import LifecycleError, canonical_digest
|
|
8
|
+
from agent_lifecycle.contracts.paths import is_under_repo_path, normalize_repo_path
|
|
9
|
+
from agent_lifecycle.planning.completeness import require_plan_completeness_pass, validate_plan_completeness
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_plan_manifest(
|
|
13
|
+
manifest: dict[str, Any],
|
|
14
|
+
*,
|
|
15
|
+
require_completeness: bool = False,
|
|
16
|
+
completeness_profile: dict[str, Any] | None = None,
|
|
17
|
+
) -> dict[str, Any]:
|
|
18
|
+
_require_package(manifest)
|
|
19
|
+
workstreams = _workstreams(manifest)
|
|
20
|
+
overlaps = _write_overlaps(workstreams)
|
|
21
|
+
if overlaps:
|
|
22
|
+
raise LifecycleError("plan-write-overlap", "workstream write sets overlap", {"overlaps": overlaps})
|
|
23
|
+
payload: dict[str, Any] = {
|
|
24
|
+
"schemaVersion": "agent-plan-validation.v1",
|
|
25
|
+
"packageId": manifest["package"]["id"],
|
|
26
|
+
"planRevision": manifest.get("planRevision"),
|
|
27
|
+
"status": manifest.get("status"),
|
|
28
|
+
"workstreamCount": len(workstreams),
|
|
29
|
+
"planDigest": canonical_digest(manifest),
|
|
30
|
+
}
|
|
31
|
+
if require_completeness:
|
|
32
|
+
completeness = validate_plan_completeness(manifest, profile=completeness_profile)
|
|
33
|
+
require_plan_completeness_pass(completeness)
|
|
34
|
+
payload["completeness"] = completeness
|
|
35
|
+
return payload
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _require_package(manifest: dict[str, Any]) -> None:
|
|
39
|
+
package = manifest.get("package")
|
|
40
|
+
if not isinstance(package, dict) or not isinstance(package.get("id"), str):
|
|
41
|
+
raise LifecycleError("invalid-plan-manifest", "package.id is required")
|
|
42
|
+
if manifest.get("status") not in {"DRAFT", "REOPENED", "FROZEN"}:
|
|
43
|
+
raise LifecycleError("invalid-plan-manifest", "plan status is invalid")
|
|
44
|
+
if not isinstance(manifest.get("planRevision"), int):
|
|
45
|
+
raise LifecycleError("invalid-plan-manifest", "planRevision is required")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _workstreams(manifest: dict[str, Any]) -> list[dict[str, Any]]:
|
|
49
|
+
workstreams = manifest.get("workstreams")
|
|
50
|
+
if not isinstance(workstreams, list) or not workstreams:
|
|
51
|
+
raise LifecycleError("invalid-plan-manifest", "workstreams are required")
|
|
52
|
+
return [item for item in workstreams if isinstance(item, dict)]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _write_overlaps(workstreams: list[dict[str, Any]]) -> list[dict[str, str]]:
|
|
56
|
+
seen: list[tuple[str, str]] = []
|
|
57
|
+
overlaps: list[dict[str, str]] = []
|
|
58
|
+
for workstream in workstreams:
|
|
59
|
+
owner = workstream.get("id")
|
|
60
|
+
for raw_path in workstream.get("writes", []):
|
|
61
|
+
path = normalize_repo_path(raw_path)
|
|
62
|
+
overlaps.extend(_overlaps(owner, path, seen))
|
|
63
|
+
seen.append((owner, path))
|
|
64
|
+
return overlaps
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _overlaps(owner: str, path: str, seen: list[tuple[str, str]]) -> list[dict[str, str]]:
|
|
68
|
+
return [
|
|
69
|
+
{"leftOwner": prior_owner, "left": prior_path, "rightOwner": owner, "right": path}
|
|
70
|
+
for prior_owner, prior_path in seen
|
|
71
|
+
if prior_owner != owner and (is_under_repo_path(path, prior_path) or is_under_repo_path(prior_path, path))
|
|
72
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Lifecycle policy proposal helpers."""
|
|
2
|
+
|
|
3
|
+
from agent_lifecycle.policy.apply import apply_policy_proposal, build_tuned_policy
|
|
4
|
+
from agent_lifecycle.policy.adaptive_lifecycle import (
|
|
5
|
+
build_adaptive_lifecycle_decision,
|
|
6
|
+
require_adaptive_lifecycle_decision_pass,
|
|
7
|
+
small_model_packet_eligibility,
|
|
8
|
+
validate_adaptive_lifecycle_decision,
|
|
9
|
+
)
|
|
10
|
+
from agent_lifecycle.policy.proposals import (
|
|
11
|
+
build_policy_proposal,
|
|
12
|
+
build_policy_summary,
|
|
13
|
+
require_policy_proposal_pass,
|
|
14
|
+
)
|
|
15
|
+
from agent_lifecycle.policy.quality_floor import quality_floor_mode, resolve_quality_floor
|
|
16
|
+
from agent_lifecycle.policy.runtime_receipts import (
|
|
17
|
+
build_runtime_policy_receipt,
|
|
18
|
+
require_runtime_policy_receipt_pass,
|
|
19
|
+
validate_runtime_policy_receipt,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"apply_policy_proposal",
|
|
24
|
+
"build_adaptive_lifecycle_decision",
|
|
25
|
+
"build_policy_proposal",
|
|
26
|
+
"build_policy_summary",
|
|
27
|
+
"build_runtime_policy_receipt",
|
|
28
|
+
"build_tuned_policy",
|
|
29
|
+
"quality_floor_mode",
|
|
30
|
+
"require_adaptive_lifecycle_decision_pass",
|
|
31
|
+
"require_policy_proposal_pass",
|
|
32
|
+
"require_runtime_policy_receipt_pass",
|
|
33
|
+
"resolve_quality_floor",
|
|
34
|
+
"small_model_packet_eligibility",
|
|
35
|
+
"validate_adaptive_lifecycle_decision",
|
|
36
|
+
"validate_runtime_policy_receipt",
|
|
37
|
+
]
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Adaptive lifecycle policy decisions from provider-neutral inputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_lifecycle.contracts import LifecycleError, canonical_digest
|
|
8
|
+
from agent_lifecycle.metrics.recommendations import validate_lifecycle_baselines
|
|
9
|
+
from agent_lifecycle.policy.quality_floor import MODES, max_mode, mode_index, resolve_quality_floor
|
|
10
|
+
from agent_lifecycle.quality.failure_classification import FAILURE_CLASSES, HIGH_RISK_FAILURE_CLASSES
|
|
11
|
+
|
|
12
|
+
ADAPTIVE_REQUEST_SCHEMA = "agent-adaptive-lifecycle-policy-request.v1"
|
|
13
|
+
ADAPTIVE_DECISION_SCHEMA = "agent-adaptive-lifecycle-policy-decision.v1"
|
|
14
|
+
ADAPTIVE_VALIDATION_SCHEMA = "agent-adaptive-lifecycle-policy-decision-validation.v1"
|
|
15
|
+
RESOURCE_BASIS = "tokens-and-resources"
|
|
16
|
+
SDD_TIERS = {"S0", "S1", "S2"}
|
|
17
|
+
BUDGET_MODES = {"local", "subscription", "metered"}
|
|
18
|
+
FLAKE_STATUSES = {"stable-fail", "stable-pass", "flaky", "inconclusive"}
|
|
19
|
+
FORBIDDEN_NEUTRAL_KEYS = {
|
|
20
|
+
"apikey",
|
|
21
|
+
"auth",
|
|
22
|
+
"authheader",
|
|
23
|
+
"credential",
|
|
24
|
+
"credentials",
|
|
25
|
+
"model",
|
|
26
|
+
"modelid",
|
|
27
|
+
"modelname",
|
|
28
|
+
"password",
|
|
29
|
+
"provider",
|
|
30
|
+
"providerid",
|
|
31
|
+
"providermodel",
|
|
32
|
+
"providermodelhash",
|
|
33
|
+
"providername",
|
|
34
|
+
"secret",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_adaptive_lifecycle_decision(request: dict[str, Any], baseline_profile: dict[str, Any]) -> dict[str, Any]:
|
|
39
|
+
"""Build a deterministic advisory-or-opt-in lifecycle mode decision."""
|
|
40
|
+
|
|
41
|
+
normalized, blockers = _normalize_request(request)
|
|
42
|
+
blockers.extend(_neutrality_blockers(request))
|
|
43
|
+
blockers.extend(_monetary_blockers(request, budget_mode=normalized["budgetMode"]))
|
|
44
|
+
baseline_validation = validate_lifecycle_baselines(baseline_profile)
|
|
45
|
+
if baseline_validation["status"] != "PASS":
|
|
46
|
+
blockers.extend({"code": "adaptive-baseline-invalid", "blocker": item} for item in baseline_validation["blockers"])
|
|
47
|
+
|
|
48
|
+
floor_decision = resolve_quality_floor(
|
|
49
|
+
task_shape=normalized["taskShape"],
|
|
50
|
+
baseline_profile=baseline_profile,
|
|
51
|
+
sdd_tier=normalized["sddTier"],
|
|
52
|
+
risk_flags=normalized["riskFlags"],
|
|
53
|
+
required_evidence=normalized["requiredEvidence"],
|
|
54
|
+
)
|
|
55
|
+
if floor_decision["status"] != "PASS":
|
|
56
|
+
blockers.extend({"code": "adaptive-quality-floor-invalid", "blocker": item} for item in floor_decision["blockers"])
|
|
57
|
+
|
|
58
|
+
recommended, reasons = _recommended_mode(normalized, baseline_profile, floor_decision["qualityFloor"])
|
|
59
|
+
apply_automatically = normalized["automaticSelectionEnabled"] and not blockers
|
|
60
|
+
body = {
|
|
61
|
+
"schemaVersion": ADAPTIVE_DECISION_SCHEMA,
|
|
62
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
63
|
+
"requestId": normalized["requestId"],
|
|
64
|
+
"taskShape": normalized["taskShape"],
|
|
65
|
+
"sddTier": normalized["sddTier"],
|
|
66
|
+
"currentMode": normalized["currentMode"],
|
|
67
|
+
"recommendedMode": recommended,
|
|
68
|
+
"selectedMode": recommended,
|
|
69
|
+
"qualityFloor": floor_decision["qualityFloor"],
|
|
70
|
+
"qualityFloorPreserved": mode_index(recommended) >= mode_index(floor_decision["qualityFloor"]),
|
|
71
|
+
"automaticSelectionEnabled": normalized["automaticSelectionEnabled"],
|
|
72
|
+
"applyAutomatically": apply_automatically,
|
|
73
|
+
"advisoryOnly": not apply_automatically,
|
|
74
|
+
"reasonCodes": [*floor_decision["reasonCodes"], *reasons],
|
|
75
|
+
"neutralInputs": _neutral_inputs(normalized, monetary_keys=_monetary_keys(request)),
|
|
76
|
+
"resourceBasis": RESOURCE_BASIS,
|
|
77
|
+
"monetaryFieldsUsed": False,
|
|
78
|
+
"providerModelNamesInCore": False,
|
|
79
|
+
"blockers": blockers,
|
|
80
|
+
"qualityFloorDecision": floor_decision,
|
|
81
|
+
"baselineValidation": baseline_validation,
|
|
82
|
+
"requestDigest": canonical_digest(request),
|
|
83
|
+
"baselineProfileDigest": canonical_digest(baseline_profile),
|
|
84
|
+
"productionPromotionClaimed": False,
|
|
85
|
+
}
|
|
86
|
+
return {**body, "decisionDigest": canonical_digest(body)}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate_adaptive_lifecycle_decision(decision: dict[str, Any]) -> dict[str, Any]:
|
|
90
|
+
if not isinstance(decision, dict):
|
|
91
|
+
raise LifecycleError("invalid-adaptive-lifecycle-decision", "adaptive lifecycle decision must be an object")
|
|
92
|
+
blockers: list[dict[str, Any]] = []
|
|
93
|
+
if decision.get("schemaVersion") != ADAPTIVE_DECISION_SCHEMA:
|
|
94
|
+
blockers.append({"code": "adaptive-decision-schema-invalid"})
|
|
95
|
+
if decision.get("status") not in {"PASS", "FAIL"}:
|
|
96
|
+
blockers.append({"code": "adaptive-decision-status-invalid"})
|
|
97
|
+
floor = decision.get("qualityFloor")
|
|
98
|
+
selected = decision.get("selectedMode")
|
|
99
|
+
recommended = decision.get("recommendedMode")
|
|
100
|
+
for field, value in [("qualityFloor", floor), ("selectedMode", selected), ("recommendedMode", recommended)]:
|
|
101
|
+
if value not in MODES:
|
|
102
|
+
blockers.append({"code": "adaptive-decision-mode-invalid", "field": field, "mode": value})
|
|
103
|
+
if floor in MODES and selected in MODES and mode_index(selected) < mode_index(floor):
|
|
104
|
+
blockers.append({"code": "adaptive-decision-selected-below-floor", "selectedMode": selected, "qualityFloor": floor})
|
|
105
|
+
if floor in MODES and recommended in MODES and mode_index(recommended) < mode_index(floor):
|
|
106
|
+
blockers.append({"code": "adaptive-decision-recommended-below-floor", "recommendedMode": recommended, "qualityFloor": floor})
|
|
107
|
+
if decision.get("qualityFloorPreserved") is not True:
|
|
108
|
+
blockers.append({"code": "adaptive-decision-quality-floor-not-preserved"})
|
|
109
|
+
if decision.get("resourceBasis") != RESOURCE_BASIS:
|
|
110
|
+
blockers.append({"code": "adaptive-decision-resource-basis-invalid"})
|
|
111
|
+
if decision.get("monetaryFieldsUsed") is not False:
|
|
112
|
+
blockers.append({"code": "adaptive-decision-monetary-fields-used"})
|
|
113
|
+
if decision.get("providerModelNamesInCore") is not False:
|
|
114
|
+
blockers.append({"code": "adaptive-decision-provider-model-core"})
|
|
115
|
+
if decision.get("productionPromotionClaimed") is not False:
|
|
116
|
+
blockers.append({"code": "adaptive-decision-production-claim"})
|
|
117
|
+
if not isinstance(decision.get("automaticSelectionEnabled"), bool):
|
|
118
|
+
blockers.append({"code": "adaptive-decision-auto-flag-invalid"})
|
|
119
|
+
if not isinstance(decision.get("applyAutomatically"), bool):
|
|
120
|
+
blockers.append({"code": "adaptive-decision-apply-flag-invalid"})
|
|
121
|
+
if not isinstance(decision.get("advisoryOnly"), bool):
|
|
122
|
+
blockers.append({"code": "adaptive-decision-advisory-flag-invalid"})
|
|
123
|
+
if decision.get("applyAutomatically") is True and decision.get("automaticSelectionEnabled") is not True:
|
|
124
|
+
blockers.append({"code": "adaptive-decision-auto-apply-without-opt-in"})
|
|
125
|
+
if decision.get("applyAutomatically") is True and decision.get("advisoryOnly") is True:
|
|
126
|
+
blockers.append({"code": "adaptive-decision-auto-apply-advisory"})
|
|
127
|
+
embedded_blockers = decision.get("blockers")
|
|
128
|
+
if not isinstance(embedded_blockers, list) or not all(isinstance(item, dict) for item in embedded_blockers):
|
|
129
|
+
blockers.append({"code": "adaptive-decision-blockers-invalid"})
|
|
130
|
+
embedded_blockers = []
|
|
131
|
+
if decision.get("status") == "PASS" and embedded_blockers:
|
|
132
|
+
blockers.append({"code": "adaptive-decision-pass-with-blockers"})
|
|
133
|
+
if decision.get("status") == "FAIL" and not embedded_blockers:
|
|
134
|
+
blockers.append({"code": "adaptive-decision-fail-without-blockers"})
|
|
135
|
+
expected_digest = canonical_digest({key: value for key, value in decision.items() if key != "decisionDigest"})
|
|
136
|
+
if decision.get("decisionDigest") != expected_digest:
|
|
137
|
+
blockers.append({"code": "adaptive-decision-digest-mismatch"})
|
|
138
|
+
body = {
|
|
139
|
+
"schemaVersion": ADAPTIVE_VALIDATION_SCHEMA,
|
|
140
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
141
|
+
"decisionStatus": decision.get("status") if isinstance(decision.get("status"), str) else None,
|
|
142
|
+
"recommendedMode": recommended if recommended in MODES else None,
|
|
143
|
+
"selectedMode": selected if selected in MODES else None,
|
|
144
|
+
"qualityFloor": floor if floor in MODES else None,
|
|
145
|
+
"qualityFloorPreserved": decision.get("qualityFloorPreserved") is True,
|
|
146
|
+
"applyAutomatically": decision.get("applyAutomatically") is True,
|
|
147
|
+
"advisoryOnly": decision.get("advisoryOnly") is True,
|
|
148
|
+
"blockers": blockers,
|
|
149
|
+
"decisionDigest": decision.get("decisionDigest") if isinstance(decision.get("decisionDigest"), str) else None,
|
|
150
|
+
}
|
|
151
|
+
return {**body, "validationDigest": canonical_digest(body)}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def require_adaptive_lifecycle_decision_pass(validation: dict[str, Any]) -> dict[str, Any]:
|
|
155
|
+
if validation.get("status") != "PASS" or validation.get("decisionStatus") != "PASS":
|
|
156
|
+
raise LifecycleError("adaptive-lifecycle-decision-validation-failed", "adaptive lifecycle decision did not pass", {"validation": validation})
|
|
157
|
+
return validation
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def small_model_packet_eligibility(decision: dict[str, Any]) -> dict[str, Any]:
|
|
161
|
+
"""Return whether a small-model packet surface is allowed by the decision."""
|
|
162
|
+
|
|
163
|
+
validation = validate_adaptive_lifecycle_decision(decision)
|
|
164
|
+
blockers = []
|
|
165
|
+
if validation.get("status") != "PASS" or validation.get("decisionStatus") != "PASS":
|
|
166
|
+
blockers.append({"code": "small-model-adaptive-decision-invalid", "validation": validation})
|
|
167
|
+
floor = decision.get("qualityFloor")
|
|
168
|
+
if floor not in {"light", "standard"}:
|
|
169
|
+
blockers.append({"code": "small-model-quality-floor-blocked", "qualityFloor": floor})
|
|
170
|
+
body = {
|
|
171
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
172
|
+
"decisionDigest": decision.get("decisionDigest"),
|
|
173
|
+
"qualityFloor": floor,
|
|
174
|
+
"recommendedMode": decision.get("recommendedMode"),
|
|
175
|
+
"smallModelPacketEligible": not blockers,
|
|
176
|
+
"advisoryOnly": decision.get("advisoryOnly") is True,
|
|
177
|
+
"blockers": blockers,
|
|
178
|
+
}
|
|
179
|
+
return {**body, "eligibilityDigest": canonical_digest(body)}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _normalize_request(request: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
183
|
+
blockers: list[dict[str, Any]] = []
|
|
184
|
+
if not isinstance(request, dict):
|
|
185
|
+
raise LifecycleError("invalid-adaptive-lifecycle-request", "adaptive lifecycle request must be an object")
|
|
186
|
+
if request.get("schemaVersion") != ADAPTIVE_REQUEST_SCHEMA:
|
|
187
|
+
blockers.append({"code": "adaptive-request-schema-invalid"})
|
|
188
|
+
task_shape = _string(request.get("taskShape"), default="feature")
|
|
189
|
+
if "taskShape" not in request:
|
|
190
|
+
blockers.append({"code": "adaptive-request-task-shape-missing"})
|
|
191
|
+
sdd_tier = _string(request.get("sddTier"), default="S1")
|
|
192
|
+
if sdd_tier not in SDD_TIERS:
|
|
193
|
+
blockers.append({"code": "adaptive-request-sdd-tier-invalid", "sddTier": sdd_tier})
|
|
194
|
+
sdd_tier = "S1"
|
|
195
|
+
risk_flags = _risk_list(request.get("riskFlags"))
|
|
196
|
+
required_evidence = _string_list(request.get("requiredEvidence", []))
|
|
197
|
+
prior_attempts = _non_negative_int(request.get("priorAttempts", 0), field="priorAttempts", blockers=blockers)
|
|
198
|
+
context_tokens = _non_negative_int(request.get("contextTokens", 0), field="contextTokens", blockers=blockers)
|
|
199
|
+
resource_caps = _resource_caps(request.get("resourceCaps", {}), blockers)
|
|
200
|
+
failure_signals = _failure_signals(request.get("failureSignals", {}), blockers)
|
|
201
|
+
budget_mode = _string(request.get("budgetMode"), default="local")
|
|
202
|
+
if budget_mode not in BUDGET_MODES:
|
|
203
|
+
blockers.append({"code": "adaptive-request-budget-mode-invalid", "budgetMode": budget_mode})
|
|
204
|
+
budget_mode = "local"
|
|
205
|
+
current_mode = request.get("currentMode")
|
|
206
|
+
if current_mode is not None and current_mode not in MODES:
|
|
207
|
+
blockers.append({"code": "adaptive-request-current-mode-invalid", "currentMode": current_mode})
|
|
208
|
+
current_mode = None
|
|
209
|
+
automatic = request.get("automaticSelectionEnabled", False)
|
|
210
|
+
if not isinstance(automatic, bool):
|
|
211
|
+
blockers.append({"code": "adaptive-request-auto-flag-invalid"})
|
|
212
|
+
automatic = False
|
|
213
|
+
return (
|
|
214
|
+
{
|
|
215
|
+
"requestId": request.get("requestId") if isinstance(request.get("requestId"), str) and request.get("requestId") else None,
|
|
216
|
+
"taskShape": task_shape,
|
|
217
|
+
"sddTier": sdd_tier,
|
|
218
|
+
"riskFlags": risk_flags,
|
|
219
|
+
"requiredEvidence": required_evidence,
|
|
220
|
+
"priorAttempts": prior_attempts,
|
|
221
|
+
"contextTokens": context_tokens,
|
|
222
|
+
"resourceCaps": resource_caps,
|
|
223
|
+
"failureSignals": failure_signals,
|
|
224
|
+
"budgetMode": budget_mode,
|
|
225
|
+
"currentMode": current_mode,
|
|
226
|
+
"automaticSelectionEnabled": automatic,
|
|
227
|
+
},
|
|
228
|
+
blockers,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _recommended_mode(normalized: dict[str, Any], baseline_profile: dict[str, Any], quality_floor: str) -> tuple[str, list[str]]:
|
|
233
|
+
reasons: list[str] = []
|
|
234
|
+
shape = {}
|
|
235
|
+
shapes = baseline_profile.get("taskShapes") if isinstance(baseline_profile, dict) else {}
|
|
236
|
+
if isinstance(shapes, dict) and isinstance(shapes.get(normalized["taskShape"]), dict):
|
|
237
|
+
shape = shapes[normalized["taskShape"]]
|
|
238
|
+
default_mode = shape.get("defaultMode") if shape.get("defaultMode") in MODES else "standard"
|
|
239
|
+
target = max_mode(default_mode, quality_floor)
|
|
240
|
+
reasons.append(f"default-mode-{default_mode}")
|
|
241
|
+
if normalized["priorAttempts"] >= 2:
|
|
242
|
+
target = max_mode(target, "strict")
|
|
243
|
+
reasons.append("retry-escalation")
|
|
244
|
+
failure_signals = normalized.get("failureSignals", {})
|
|
245
|
+
failure_class = failure_signals.get("failureClass")
|
|
246
|
+
if failure_class in {"api-contract", "serialization", "permission", "migration", "performance"}:
|
|
247
|
+
target = max_mode(target, "standard")
|
|
248
|
+
reasons.append(f"failure-class-{failure_class}-escalation")
|
|
249
|
+
if failure_class in HIGH_RISK_FAILURE_CLASSES or failure_class == "flaky-test" or failure_signals.get("flakeStatus") == "flaky":
|
|
250
|
+
target = max_mode(target, "strict")
|
|
251
|
+
reasons.append(f"failure-class-{failure_class or 'flake'}-strict-escalation")
|
|
252
|
+
if failure_signals.get("validationStatus") in {"FAIL", "ERROR", "BLOCKED", "REJECTED"}:
|
|
253
|
+
target = max_mode(target, "standard")
|
|
254
|
+
reasons.append("validation-failure-escalation")
|
|
255
|
+
if int(failure_signals.get("retryCount", 0)) >= 2 or int(failure_signals.get("remediationLoops", 0)) >= 2:
|
|
256
|
+
target = max_mode(target, "strict")
|
|
257
|
+
reasons.append("failure-loop-strict-escalation")
|
|
258
|
+
if normalized["currentMode"] in MODES and (normalized["priorAttempts"] > 0 or failure_signals):
|
|
259
|
+
raised = max_mode(target, normalized["currentMode"])
|
|
260
|
+
if raised != target:
|
|
261
|
+
reasons.append("no-downgrade-after-failure")
|
|
262
|
+
target = raised
|
|
263
|
+
if normalized["contextTokens"] >= 64000:
|
|
264
|
+
target = max_mode(target, "strict")
|
|
265
|
+
reasons.append("large-context-escalation")
|
|
266
|
+
elif normalized["contextTokens"] >= 32000:
|
|
267
|
+
target = max_mode(target, "standard")
|
|
268
|
+
reasons.append("medium-context-floor")
|
|
269
|
+
caps = normalized["resourceCaps"]
|
|
270
|
+
if caps.get("maxInvocations") == 1 and quality_floor in {"light", "standard"} and not _high_risk(normalized, shape):
|
|
271
|
+
target = quality_floor
|
|
272
|
+
reasons.append("resource-cap-tight")
|
|
273
|
+
return max_mode(target, quality_floor), reasons
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _neutral_inputs(normalized: dict[str, Any], *, monetary_keys: list[str]) -> dict[str, Any]:
|
|
277
|
+
caps = normalized["resourceCaps"]
|
|
278
|
+
return {
|
|
279
|
+
"taskShape": normalized["taskShape"],
|
|
280
|
+
"sddTier": normalized["sddTier"],
|
|
281
|
+
"riskFlags": list(normalized["riskFlags"]),
|
|
282
|
+
"requiredEvidence": list(normalized["requiredEvidence"]),
|
|
283
|
+
"priorAttempts": normalized["priorAttempts"],
|
|
284
|
+
"contextTokens": normalized["contextTokens"],
|
|
285
|
+
"resourceCaps": {key: caps[key] for key in sorted(caps) if key in {"maxInvocations", "maxWallSeconds", "maxBillableTokens"}},
|
|
286
|
+
"failureSignals": dict(normalized["failureSignals"]),
|
|
287
|
+
"budgetMode": normalized["budgetMode"],
|
|
288
|
+
"automaticSelectionEnabled": normalized["automaticSelectionEnabled"],
|
|
289
|
+
"monetaryMetadataKeys": monetary_keys,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _resource_caps(value: Any, blockers: list[dict[str, Any]]) -> dict[str, Any]:
|
|
294
|
+
if value is None:
|
|
295
|
+
return {}
|
|
296
|
+
if not isinstance(value, dict):
|
|
297
|
+
blockers.append({"code": "adaptive-request-resource-caps-invalid"})
|
|
298
|
+
return {}
|
|
299
|
+
caps = dict(value)
|
|
300
|
+
for field in ("maxInvocations", "maxWallSeconds", "maxBillableTokens"):
|
|
301
|
+
if field in caps:
|
|
302
|
+
caps[field] = _non_negative_int(caps[field], field=f"resourceCaps.{field}", blockers=blockers)
|
|
303
|
+
return caps
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _failure_signals(value: Any, blockers: list[dict[str, Any]]) -> dict[str, Any]:
|
|
307
|
+
if value in (None, {}):
|
|
308
|
+
return {}
|
|
309
|
+
if not isinstance(value, dict):
|
|
310
|
+
blockers.append({"code": "adaptive-request-failure-signals-invalid"})
|
|
311
|
+
return {}
|
|
312
|
+
failure_class = value.get("failureClass")
|
|
313
|
+
if failure_class is not None and failure_class not in FAILURE_CLASSES:
|
|
314
|
+
blockers.append({"code": "adaptive-request-failure-class-invalid", "failureClass": failure_class})
|
|
315
|
+
failure_class = None
|
|
316
|
+
confidence = value.get("confidence")
|
|
317
|
+
if confidence is not None and confidence not in {"LOW", "MEDIUM", "HIGH"}:
|
|
318
|
+
blockers.append({"code": "adaptive-request-failure-confidence-invalid", "confidence": confidence})
|
|
319
|
+
confidence = None
|
|
320
|
+
flake_status = value.get("flakeStatus")
|
|
321
|
+
if flake_status is not None and flake_status not in FLAKE_STATUSES:
|
|
322
|
+
blockers.append({"code": "adaptive-request-flake-status-invalid", "flakeStatus": flake_status})
|
|
323
|
+
flake_status = None
|
|
324
|
+
validation_status = value.get("validationStatus")
|
|
325
|
+
if validation_status is not None and not isinstance(validation_status, str):
|
|
326
|
+
blockers.append({"code": "adaptive-request-validation-status-invalid"})
|
|
327
|
+
validation_status = None
|
|
328
|
+
classification_digest = value.get("classificationDigest")
|
|
329
|
+
if classification_digest is not None and not _is_digest(classification_digest):
|
|
330
|
+
blockers.append({"code": "adaptive-request-classification-digest-invalid"})
|
|
331
|
+
classification_digest = None
|
|
332
|
+
return {
|
|
333
|
+
"failureClass": failure_class,
|
|
334
|
+
"confidence": confidence,
|
|
335
|
+
"retryCount": _non_negative_int(value.get("retryCount", 0), field="failureSignals.retryCount", blockers=blockers),
|
|
336
|
+
"remediationLoops": _non_negative_int(value.get("remediationLoops", 0), field="failureSignals.remediationLoops", blockers=blockers),
|
|
337
|
+
"validationStatus": validation_status,
|
|
338
|
+
"flakeStatus": flake_status,
|
|
339
|
+
"classificationDigest": classification_digest,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _monetary_blockers(value: Any, *, budget_mode: str) -> list[dict[str, Any]]:
|
|
344
|
+
blockers: list[dict[str, Any]] = []
|
|
345
|
+
for path, item in _walk(value):
|
|
346
|
+
key = path.rsplit(".", 1)[-1]
|
|
347
|
+
if _is_monetary_key(key) and item is not None and budget_mode != "metered":
|
|
348
|
+
blockers.append({"code": "adaptive-request-monetary-field-not-metered", "path": path})
|
|
349
|
+
return blockers
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _monetary_keys(value: Any) -> list[str]:
|
|
353
|
+
return sorted({path for path, item in _walk(value) if _is_monetary_key(path.rsplit(".", 1)[-1]) and item is not None})
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _neutrality_blockers(value: Any) -> list[dict[str, Any]]:
|
|
357
|
+
blockers = []
|
|
358
|
+
for path, _item in _walk(value):
|
|
359
|
+
key = path.rsplit(".", 1)[-1]
|
|
360
|
+
normalized = key.replace("_", "").replace("-", "").lower()
|
|
361
|
+
if normalized in FORBIDDEN_NEUTRAL_KEYS:
|
|
362
|
+
blockers.append({"code": "adaptive-request-provider-model-key", "path": path})
|
|
363
|
+
return blockers
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _walk(value: Any, prefix: str = "$") -> list[tuple[str, Any]]:
|
|
367
|
+
rows: list[tuple[str, Any]] = []
|
|
368
|
+
if isinstance(value, dict):
|
|
369
|
+
for key, item in value.items():
|
|
370
|
+
path = f"{prefix}.{key}"
|
|
371
|
+
rows.append((path, item))
|
|
372
|
+
rows.extend(_walk(item, path))
|
|
373
|
+
elif isinstance(value, list):
|
|
374
|
+
for index, item in enumerate(value):
|
|
375
|
+
rows.extend(_walk(item, f"{prefix}[{index}]"))
|
|
376
|
+
return rows
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _is_monetary_key(key: str) -> bool:
|
|
380
|
+
normalized = key.replace("_", "").replace("-", "").lower()
|
|
381
|
+
return "usd" in normalized or "cost" in normalized
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _is_digest(value: Any) -> bool:
|
|
385
|
+
return isinstance(value, str) and len(value) == 64 and all(char in "0123456789abcdef" for char in value)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _risk_list(value: Any) -> list[str]:
|
|
389
|
+
if isinstance(value, dict):
|
|
390
|
+
return sorted(str(key) for key, active in value.items() if active)
|
|
391
|
+
if isinstance(value, list):
|
|
392
|
+
return sorted({item for item in value if isinstance(item, str) and item})
|
|
393
|
+
return []
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _string_list(value: Any) -> list[str]:
|
|
397
|
+
if not isinstance(value, list):
|
|
398
|
+
return []
|
|
399
|
+
return sorted({item for item in value if isinstance(item, str) and item})
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _string(value: Any, *, default: str) -> str:
|
|
403
|
+
return value if isinstance(value, str) and value else default
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _non_negative_int(value: Any, *, field: str, blockers: list[dict[str, Any]]) -> int:
|
|
407
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
408
|
+
blockers.append({"code": "adaptive-request-non-negative-int-invalid", "field": field, "value": value})
|
|
409
|
+
return 0
|
|
410
|
+
return value
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _high_risk(normalized: dict[str, Any], shape: dict[str, Any]) -> bool:
|
|
414
|
+
if shape.get("highRisk") is True:
|
|
415
|
+
return True
|
|
416
|
+
return bool(set(normalized["riskFlags"]).intersection({"security", "contracts", "adapter", "architecture", "release"}))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Explicit write path for approved lifecycle policy proposals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_lifecycle.contracts import LifecycleError, canonical_digest, write_json_create
|
|
9
|
+
|
|
10
|
+
APPLY_RESULT_SCHEMA = "agent-lifecycle-policy-apply-result.v1"
|
|
11
|
+
TUNED_POLICY_SCHEMA = "agent-lifecycle-tuned-policy.v1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_tuned_policy(proposal: dict[str, Any]) -> dict[str, Any]:
|
|
15
|
+
if proposal.get("applyAllowed") is not True:
|
|
16
|
+
raise LifecycleError("policy-apply-not-allowed", "policy proposal is not safe to apply", {"proposal": proposal})
|
|
17
|
+
changes = [item for item in proposal.get("candidateChanges", []) if isinstance(item, dict) and item.get("applies") is True]
|
|
18
|
+
body = {
|
|
19
|
+
"schemaVersion": TUNED_POLICY_SCHEMA,
|
|
20
|
+
"status": "PASS",
|
|
21
|
+
"sourceProposalDigest": proposal.get("proposalDigest") or canonical_digest(proposal),
|
|
22
|
+
"changes": changes,
|
|
23
|
+
"rollback": proposal.get("rollback"),
|
|
24
|
+
"qualityConstraints": proposal.get("qualityConstraints"),
|
|
25
|
+
"productionPromotionClaimed": False,
|
|
26
|
+
}
|
|
27
|
+
return {**body, "policyDigest": canonical_digest(body)}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def apply_policy_proposal(proposal: dict[str, Any], output_path: Path) -> dict[str, Any]:
|
|
31
|
+
policy = build_tuned_policy(proposal)
|
|
32
|
+
payload = write_json_create(output_path, policy)
|
|
33
|
+
body = {
|
|
34
|
+
"schemaVersion": APPLY_RESULT_SCHEMA,
|
|
35
|
+
"status": "PASS",
|
|
36
|
+
"outputPath": output_path.as_posix(),
|
|
37
|
+
"outputBytes": len(payload),
|
|
38
|
+
"outputDigest": canonical_digest(policy),
|
|
39
|
+
"proposalDigest": proposal.get("proposalDigest") or canonical_digest(proposal),
|
|
40
|
+
"changed": bool(policy["changes"]),
|
|
41
|
+
"liveCallsStarted": False,
|
|
42
|
+
"productionPromotionClaimed": False,
|
|
43
|
+
}
|
|
44
|
+
return {**body, "applyDigest": canonical_digest(body)}
|