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,244 @@
|
|
|
1
|
+
"""Goal record validation and compact continuation snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_lifecycle.context.profiles import resolve_window, validate_context_profile
|
|
9
|
+
from agent_lifecycle.context.rendering import estimate_tokens
|
|
10
|
+
from agent_lifecycle.contracts import LifecycleError, canonical_digest
|
|
11
|
+
|
|
12
|
+
GOAL_STATUSES = {"ACTIVE", "BLOCKED", "READY_FOR_FINALIZATION", "COMPLETE"}
|
|
13
|
+
LINEAGE_KEYS = ("runId", "packageId", "planRevision", "planDigest", "sourceRevision")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_goal_record(
|
|
17
|
+
record: dict[str, Any],
|
|
18
|
+
*,
|
|
19
|
+
state: dict[str, Any] | None = None,
|
|
20
|
+
require_current: bool = False,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
"""Validate a goal record and optionally bind it to current workflow state."""
|
|
23
|
+
|
|
24
|
+
if not isinstance(record, dict):
|
|
25
|
+
raise LifecycleError("invalid-goal-record", "goal record must be an object")
|
|
26
|
+
if record.get("schemaVersion") != "agent-goal-record.v1":
|
|
27
|
+
raise LifecycleError("invalid-goal-record", "goal record schemaVersion is unsupported")
|
|
28
|
+
goal_id = _required_string(record.get("goalId"), label="goalId")
|
|
29
|
+
_required_string(record.get("userIntent"), label="userIntent")
|
|
30
|
+
_required_string(record.get("ownerOutcome"), label="ownerOutcome")
|
|
31
|
+
status = record.get("status")
|
|
32
|
+
if status not in GOAL_STATUSES:
|
|
33
|
+
raise LifecycleError("invalid-goal-record", "goal status is unsupported")
|
|
34
|
+
constraints = _string_list(record.get("constraints"), label="constraints")
|
|
35
|
+
evidence_ids = _string_list(record.get("evidenceIds"), label="evidenceIds", allow_empty=True)
|
|
36
|
+
lineage = _lineage(record)
|
|
37
|
+
completion_check = _completion_check(record.get("completionCheck"))
|
|
38
|
+
if state is not None:
|
|
39
|
+
_validate_state_binding(lineage, completion_check, state, require_current=require_current)
|
|
40
|
+
return {
|
|
41
|
+
"schemaVersion": "agent-goal-record-validation.v1",
|
|
42
|
+
"status": "PASS",
|
|
43
|
+
"goalId": goal_id,
|
|
44
|
+
"goalStatus": status,
|
|
45
|
+
"lineage": lineage,
|
|
46
|
+
"stateRevision": lineage.get("stateRevision"),
|
|
47
|
+
"constraintCount": len(constraints),
|
|
48
|
+
"evidenceIds": evidence_ids,
|
|
49
|
+
"completionCheck": completion_check,
|
|
50
|
+
"goalDigest": canonical_digest(record),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_objective_snapshot(
|
|
55
|
+
record: dict[str, Any],
|
|
56
|
+
state: dict[str, Any],
|
|
57
|
+
*,
|
|
58
|
+
profile: dict[str, Any] | None = None,
|
|
59
|
+
window: str | None = None,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Build a compact continuation summary from a current goal record."""
|
|
62
|
+
|
|
63
|
+
from agent_lifecycle.workflow.query import next_action
|
|
64
|
+
|
|
65
|
+
validation = validate_goal_record(record, state=state, require_current=True)
|
|
66
|
+
body: dict[str, Any] = {
|
|
67
|
+
"schemaVersion": "agent-objective-snapshot.v1",
|
|
68
|
+
"status": "PASS",
|
|
69
|
+
"goal": {
|
|
70
|
+
"goalId": validation["goalId"],
|
|
71
|
+
"goalStatus": validation["goalStatus"],
|
|
72
|
+
"goalDigest": validation["goalDigest"],
|
|
73
|
+
"userIntent": record.get("userIntent"),
|
|
74
|
+
"userIntentDigest": canonical_digest({"userIntent": record.get("userIntent")}),
|
|
75
|
+
"ownerOutcome": record.get("ownerOutcome"),
|
|
76
|
+
"constraints": record.get("constraints", []),
|
|
77
|
+
"constraintsDigest": canonical_digest({"constraints": record.get("constraints", [])}),
|
|
78
|
+
"evidenceIds": validation["evidenceIds"],
|
|
79
|
+
},
|
|
80
|
+
"lifecycle": {
|
|
81
|
+
"runId": state.get("runId"),
|
|
82
|
+
"packageId": state.get("packageId"),
|
|
83
|
+
"planRevision": state.get("planRevision"),
|
|
84
|
+
"planDigest": state.get("planDigest"),
|
|
85
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
86
|
+
"phase": state.get("phase"),
|
|
87
|
+
"stateRevision": state.get("stateRevision"),
|
|
88
|
+
"nextAction": next_action(state),
|
|
89
|
+
},
|
|
90
|
+
"completionCheck": validation["completionCheck"],
|
|
91
|
+
}
|
|
92
|
+
token_estimate = estimate_tokens(body)
|
|
93
|
+
target: dict[str, Any] | None = None
|
|
94
|
+
if profile is not None:
|
|
95
|
+
validate_context_profile(profile)
|
|
96
|
+
selected = resolve_window(profile, window)
|
|
97
|
+
limit = selected["limits"]["maxStateSummaryTokens"]
|
|
98
|
+
target = {"window": selected["name"], "limit": limit}
|
|
99
|
+
if token_estimate > limit:
|
|
100
|
+
raise LifecycleError(
|
|
101
|
+
"goal-summary-overflow",
|
|
102
|
+
"objective snapshot exceeds compact context state-summary limit",
|
|
103
|
+
{"estimatedTokens": token_estimate, "limit": limit, "window": selected["name"]},
|
|
104
|
+
)
|
|
105
|
+
body["estimatedTokens"] = token_estimate
|
|
106
|
+
if target is not None:
|
|
107
|
+
body["target"] = target
|
|
108
|
+
return {**body, "snapshotDigest": canonical_digest(body)}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def update_goal_record(
|
|
112
|
+
record: dict[str, Any],
|
|
113
|
+
state: dict[str, Any],
|
|
114
|
+
*,
|
|
115
|
+
status: str | None = None,
|
|
116
|
+
reason: str,
|
|
117
|
+
evidence_ids: list[str] | None = None,
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
"""Return a new goal record bound to the current workflow state."""
|
|
120
|
+
|
|
121
|
+
previous = validate_goal_record(record, state=state, require_current=False)
|
|
122
|
+
new_status = status or str(record.get("status"))
|
|
123
|
+
if new_status not in GOAL_STATUSES:
|
|
124
|
+
raise LifecycleError("invalid-goal-record", "goal status is unsupported")
|
|
125
|
+
added_evidence = _string_list(evidence_ids or [], label="evidenceIds", allow_empty=True)
|
|
126
|
+
merged_evidence = sorted(set(previous["evidenceIds"]) | set(added_evidence))
|
|
127
|
+
updated = {
|
|
128
|
+
**record,
|
|
129
|
+
"status": new_status,
|
|
130
|
+
"lineage": _lineage_from_state(state),
|
|
131
|
+
"completionCheck": _state_completion_check(state),
|
|
132
|
+
"evidenceIds": merged_evidence,
|
|
133
|
+
"updatedAt": _now_iso(),
|
|
134
|
+
}
|
|
135
|
+
if updated["completionCheck"] is None:
|
|
136
|
+
updated.pop("completionCheck", None)
|
|
137
|
+
history = list(record.get("history", [])) if isinstance(record.get("history"), list) else []
|
|
138
|
+
history.append({
|
|
139
|
+
"previousGoalDigest": previous["goalDigest"],
|
|
140
|
+
"previousStateRevision": previous["stateRevision"],
|
|
141
|
+
"reason": reason,
|
|
142
|
+
"updatedAt": updated["updatedAt"],
|
|
143
|
+
})
|
|
144
|
+
updated["history"] = history
|
|
145
|
+
validate_goal_record(updated, state=state, require_current=True)
|
|
146
|
+
return updated
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _validate_state_binding(
|
|
150
|
+
lineage: dict[str, Any],
|
|
151
|
+
completion_check: dict[str, Any] | None,
|
|
152
|
+
state: dict[str, Any],
|
|
153
|
+
*,
|
|
154
|
+
require_current: bool,
|
|
155
|
+
) -> None:
|
|
156
|
+
for key in LINEAGE_KEYS:
|
|
157
|
+
if lineage.get(key) != state.get(key):
|
|
158
|
+
raise LifecycleError("goal-lineage-mismatch", f"goal record {key} mismatch")
|
|
159
|
+
state_revision = lineage.get("stateRevision")
|
|
160
|
+
if not isinstance(state_revision, int) or isinstance(state_revision, bool) or state_revision < 1:
|
|
161
|
+
raise LifecycleError("invalid-goal-record", "goal lineage.stateRevision must be a positive integer")
|
|
162
|
+
if require_current and state_revision != state.get("stateRevision"):
|
|
163
|
+
raise LifecycleError(
|
|
164
|
+
"goal-state-stale",
|
|
165
|
+
"goal record is not bound to the current workflow state revision",
|
|
166
|
+
{"goalStateRevision": state_revision, "stateRevision": state.get("stateRevision")},
|
|
167
|
+
)
|
|
168
|
+
expected_check = _state_completion_check(state)
|
|
169
|
+
if expected_check != completion_check:
|
|
170
|
+
raise LifecycleError("goal-completion-check-mismatch", "goal record completionCheck does not match workflow state")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _lineage(record: dict[str, Any]) -> dict[str, Any]:
|
|
174
|
+
lineage = record.get("lineage")
|
|
175
|
+
if not isinstance(lineage, dict):
|
|
176
|
+
raise LifecycleError("invalid-goal-record", "goal lineage is required")
|
|
177
|
+
result = {key: lineage.get(key) for key in LINEAGE_KEYS}
|
|
178
|
+
for key, value in result.items():
|
|
179
|
+
if key == "planRevision":
|
|
180
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
|
|
181
|
+
raise LifecycleError("invalid-goal-record", "goal lineage.planRevision must be a positive integer")
|
|
182
|
+
elif not isinstance(value, str) or not value:
|
|
183
|
+
raise LifecycleError("invalid-goal-record", f"goal lineage.{key} is required")
|
|
184
|
+
state_revision = lineage.get("stateRevision")
|
|
185
|
+
if not isinstance(state_revision, int) or isinstance(state_revision, bool) or state_revision < 1:
|
|
186
|
+
raise LifecycleError("invalid-goal-record", "goal lineage.stateRevision must be a positive integer")
|
|
187
|
+
result["stateRevision"] = state_revision
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _lineage_from_state(state: dict[str, Any]) -> dict[str, Any]:
|
|
192
|
+
return {
|
|
193
|
+
"runId": state.get("runId"),
|
|
194
|
+
"packageId": state.get("packageId"),
|
|
195
|
+
"planRevision": state.get("planRevision"),
|
|
196
|
+
"planDigest": state.get("planDigest"),
|
|
197
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
198
|
+
"stateRevision": state.get("stateRevision"),
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _state_completion_check(state: dict[str, Any]) -> dict[str, Any] | None:
|
|
203
|
+
validation = state.get("completionCheckValidation")
|
|
204
|
+
if not isinstance(validation, dict):
|
|
205
|
+
return None
|
|
206
|
+
check_id = validation.get("checkId")
|
|
207
|
+
check_digest = validation.get("checkDigest")
|
|
208
|
+
if not isinstance(check_id, str) or not isinstance(check_digest, str):
|
|
209
|
+
return None
|
|
210
|
+
return {"checkId": check_id, "checkDigest": check_digest}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _completion_check(value: Any) -> dict[str, Any] | None:
|
|
214
|
+
if value is None:
|
|
215
|
+
return None
|
|
216
|
+
if not isinstance(value, dict):
|
|
217
|
+
raise LifecycleError("invalid-goal-record", "goal completionCheck must be an object")
|
|
218
|
+
check_id = _required_string(value.get("checkId"), label="completionCheck.checkId")
|
|
219
|
+
check_digest = _digest(value.get("checkDigest"), label="completionCheck.checkDigest")
|
|
220
|
+
return {"checkId": check_id, "checkDigest": check_digest}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _required_string(value: Any, *, label: str) -> str:
|
|
224
|
+
if not isinstance(value, str) or not value:
|
|
225
|
+
raise LifecycleError("invalid-goal-record", f"{label} is required")
|
|
226
|
+
return value
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _digest(value: Any, *, label: str) -> str:
|
|
230
|
+
if not isinstance(value, str) or len(value) != 64:
|
|
231
|
+
raise LifecycleError("invalid-goal-record", f"{label} must be a 64-character digest")
|
|
232
|
+
return value
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _string_list(value: Any, *, label: str, allow_empty: bool = False) -> list[str]:
|
|
236
|
+
if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value):
|
|
237
|
+
raise LifecycleError("invalid-goal-record", f"{label} must be a list of non-empty strings")
|
|
238
|
+
if not value and not allow_empty:
|
|
239
|
+
raise LifecycleError("invalid-goal-record", f"{label} must not be empty")
|
|
240
|
+
return list(value)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _now_iso() -> str:
|
|
244
|
+
return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Read-only goal and lifecycle progress view."""
|
|
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, read_json_object
|
|
9
|
+
from agent_lifecycle.goal.records import validate_goal_record
|
|
10
|
+
from agent_lifecycle.reporting.progress_view import build_lifecycle_progress_view
|
|
11
|
+
from agent_lifecycle.workflow.query import next_action
|
|
12
|
+
from agent_lifecycle.workflow.state import load_state
|
|
13
|
+
|
|
14
|
+
GOAL_PROGRESS_VIEW_SCHEMA = "agent-goal-progress-view.v1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_goal_progress_view(
|
|
18
|
+
*,
|
|
19
|
+
record_path: Path,
|
|
20
|
+
state_path: Path,
|
|
21
|
+
usage_receipt_paths: list[Path] | None = None,
|
|
22
|
+
change_summary_path: Path | None = None,
|
|
23
|
+
require_current: bool = True,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Build a compact read-only view from goal and progress artifacts."""
|
|
26
|
+
|
|
27
|
+
record = read_json_object(record_path, label="goal record")
|
|
28
|
+
state = load_state(state_path)
|
|
29
|
+
validation = validate_goal_record(record, state=state, require_current=require_current)
|
|
30
|
+
progress = build_lifecycle_progress_view(
|
|
31
|
+
state_path=state_path,
|
|
32
|
+
usage_receipt_paths=usage_receipt_paths or [],
|
|
33
|
+
change_summary_path=change_summary_path,
|
|
34
|
+
)
|
|
35
|
+
task_summary = _task_summary(state)
|
|
36
|
+
blockers = _blockers(state, task_summary)
|
|
37
|
+
body = {
|
|
38
|
+
"schemaVersion": GOAL_PROGRESS_VIEW_SCHEMA,
|
|
39
|
+
"status": "PASS",
|
|
40
|
+
"sourceOfTruth": False,
|
|
41
|
+
"readOnly": True,
|
|
42
|
+
"modelCallsStarted": False,
|
|
43
|
+
"hostCallsStarted": False,
|
|
44
|
+
"stateMutated": False,
|
|
45
|
+
"goalRecordMutated": False,
|
|
46
|
+
"stateWritten": False,
|
|
47
|
+
"goalRecordWritten": False,
|
|
48
|
+
"tokenSpendForView": False,
|
|
49
|
+
"goal": {
|
|
50
|
+
"goalId": validation["goalId"],
|
|
51
|
+
"goalStatus": validation["goalStatus"],
|
|
52
|
+
"goalDigest": validation["goalDigest"],
|
|
53
|
+
"userIntentDigest": canonical_digest({"userIntent": record.get("userIntent")}),
|
|
54
|
+
"ownerOutcomeDigest": canonical_digest({"ownerOutcome": record.get("ownerOutcome")}),
|
|
55
|
+
"constraintCount": validation["constraintCount"],
|
|
56
|
+
"evidenceIds": validation["evidenceIds"],
|
|
57
|
+
"stateRevision": validation["stateRevision"],
|
|
58
|
+
},
|
|
59
|
+
"lifecycle": {
|
|
60
|
+
"runId": state.get("runId"),
|
|
61
|
+
"packageId": state.get("packageId"),
|
|
62
|
+
"planRevision": state.get("planRevision"),
|
|
63
|
+
"planDigest": state.get("planDigest"),
|
|
64
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
65
|
+
"phase": state.get("phase"),
|
|
66
|
+
"stateRevision": state.get("stateRevision"),
|
|
67
|
+
"nextAction": next_action(state),
|
|
68
|
+
"taskSummary": task_summary,
|
|
69
|
+
},
|
|
70
|
+
"progress": {
|
|
71
|
+
"progressDigest": progress["progressDigest"],
|
|
72
|
+
"rowCount": progress["rowCount"],
|
|
73
|
+
"lines": progress["lines"],
|
|
74
|
+
"terminalSummary": progress["terminalSummary"],
|
|
75
|
+
},
|
|
76
|
+
"metrics": {
|
|
77
|
+
"duration": progress["terminalSummary"]["duration"],
|
|
78
|
+
"tokens": progress["terminalSummary"]["tokens"],
|
|
79
|
+
"changeSummary": progress["terminalSummary"]["changeSummary"],
|
|
80
|
+
},
|
|
81
|
+
"blockers": blockers,
|
|
82
|
+
"productionPromotionClaimed": False,
|
|
83
|
+
}
|
|
84
|
+
return {**body, "viewDigest": canonical_digest(body)}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _task_summary(state: dict[str, Any]) -> dict[str, int]:
|
|
88
|
+
counts: dict[str, int] = {}
|
|
89
|
+
tasks = state.get("tasks")
|
|
90
|
+
for task in tasks if isinstance(tasks, list) else []:
|
|
91
|
+
if not isinstance(task, dict):
|
|
92
|
+
continue
|
|
93
|
+
status = str(task.get("status") or "UNKNOWN")
|
|
94
|
+
counts[status] = counts.get(status, 0) + 1
|
|
95
|
+
return counts
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _blockers(state: dict[str, Any], task_summary: dict[str, int]) -> list[dict[str, Any]]:
|
|
99
|
+
blockers: list[dict[str, Any]] = []
|
|
100
|
+
blocker = state.get("blocker")
|
|
101
|
+
if isinstance(blocker, dict):
|
|
102
|
+
blockers.append({
|
|
103
|
+
"code": str(blocker.get("code") or "workflow-blocked"),
|
|
104
|
+
"message": str(blocker.get("message") or blocker.get("reason") or "workflow is blocked"),
|
|
105
|
+
})
|
|
106
|
+
blocked_count = task_summary.get("BLOCKED", 0)
|
|
107
|
+
if blocked_count:
|
|
108
|
+
blockers.append({"code": "blocked-tasks-present", "count": blocked_count})
|
|
109
|
+
return blockers
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Provider-neutral host operation protocol."""
|
|
2
|
+
|
|
3
|
+
from agent_lifecycle.host_protocol.acp_capability import (
|
|
4
|
+
build_acp_capability,
|
|
5
|
+
build_acp_probe_receipt,
|
|
6
|
+
require_host_capabilities_pass,
|
|
7
|
+
validate_host_capabilities,
|
|
8
|
+
validate_no_acp_evidence_for_hosts,
|
|
9
|
+
)
|
|
10
|
+
from agent_lifecycle.host_protocol.capabilities import (
|
|
11
|
+
build_capability_manifest,
|
|
12
|
+
validate_capability_manifest,
|
|
13
|
+
)
|
|
14
|
+
from agent_lifecycle.host_protocol.contracts import HostAdapterEvent, HostOperationReceipt, HostOperationRequest
|
|
15
|
+
from agent_lifecycle.host_protocol.event_capture import (
|
|
16
|
+
adapter_declares_event_capture,
|
|
17
|
+
build_adapter_event_stream,
|
|
18
|
+
build_event_stream_receipt,
|
|
19
|
+
event_capture_declaration,
|
|
20
|
+
require_event_capture_pass,
|
|
21
|
+
validate_event_capture_conformance,
|
|
22
|
+
validate_event_capture_receipt,
|
|
23
|
+
)
|
|
24
|
+
from agent_lifecycle.host_protocol.events import (
|
|
25
|
+
require_adapter_event_stream_pass,
|
|
26
|
+
validate_adapter_event_stream,
|
|
27
|
+
)
|
|
28
|
+
from agent_lifecycle.host_protocol.inspection import (
|
|
29
|
+
inspect_adapter_descriptor,
|
|
30
|
+
require_adapter_inspection_pass,
|
|
31
|
+
)
|
|
32
|
+
from agent_lifecycle.host_protocol.receipts import normalize_host_operation_receipt
|
|
33
|
+
from agent_lifecycle.host_protocol.scaffold import scaffold_adapter
|
|
34
|
+
from agent_lifecycle.host_protocol.validation import (
|
|
35
|
+
require_adapter_validation_pass,
|
|
36
|
+
validate_adapter_descriptor,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"HostAdapterEvent",
|
|
41
|
+
"HostOperationReceipt",
|
|
42
|
+
"HostOperationRequest",
|
|
43
|
+
"adapter_declares_event_capture",
|
|
44
|
+
"build_acp_capability",
|
|
45
|
+
"build_acp_probe_receipt",
|
|
46
|
+
"build_capability_manifest",
|
|
47
|
+
"build_adapter_event_stream",
|
|
48
|
+
"build_event_stream_receipt",
|
|
49
|
+
"event_capture_declaration",
|
|
50
|
+
"normalize_host_operation_receipt",
|
|
51
|
+
"require_event_capture_pass",
|
|
52
|
+
"require_adapter_event_stream_pass",
|
|
53
|
+
"require_adapter_inspection_pass",
|
|
54
|
+
"require_adapter_validation_pass",
|
|
55
|
+
"require_host_capabilities_pass",
|
|
56
|
+
"inspect_adapter_descriptor",
|
|
57
|
+
"scaffold_adapter",
|
|
58
|
+
"validate_capability_manifest",
|
|
59
|
+
"validate_event_capture_conformance",
|
|
60
|
+
"validate_event_capture_receipt",
|
|
61
|
+
"validate_adapter_event_stream",
|
|
62
|
+
"validate_adapter_descriptor",
|
|
63
|
+
"validate_host_capabilities",
|
|
64
|
+
"validate_no_acp_evidence_for_hosts",
|
|
65
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Neutral ACP host capability declarations and fail-closed probe receipts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_lifecycle.contracts.errors import LifecycleError
|
|
8
|
+
|
|
9
|
+
ACP_CAPABILITY_ID = "acp"
|
|
10
|
+
HOST_CAPABILITY_SCHEMA_VERSION = "agent-host-capability.v1"
|
|
11
|
+
HOST_CAPABILITY_VALIDATION_SCHEMA_VERSION = "agent-host-capability-validation.v1"
|
|
12
|
+
ACP_PROBE_RECEIPT_SCHEMA_VERSION = "agent-acp-probe-receipt.v1"
|
|
13
|
+
_SUPPORTED_VALUES = {"supported", "unsupported", "unknown"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_acp_capability(
|
|
17
|
+
*,
|
|
18
|
+
adapter_id: str,
|
|
19
|
+
host: str,
|
|
20
|
+
support: str,
|
|
21
|
+
probe_command: list[str] | None = None,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Build a schema-backed ACP capability declaration without provider identity."""
|
|
24
|
+
|
|
25
|
+
capability: dict[str, Any] = {
|
|
26
|
+
"schemaVersion": HOST_CAPABILITY_SCHEMA_VERSION,
|
|
27
|
+
"capabilityId": ACP_CAPABILITY_ID,
|
|
28
|
+
"adapterId": adapter_id,
|
|
29
|
+
"host": host,
|
|
30
|
+
"support": support,
|
|
31
|
+
"transport": "acp" if support == "supported" else "unknown",
|
|
32
|
+
"evidencePolicy": "probe-required" if support == "supported" else "not-claimed",
|
|
33
|
+
"providerIdentityUsed": False,
|
|
34
|
+
"probe": None,
|
|
35
|
+
"invocationContract": None,
|
|
36
|
+
}
|
|
37
|
+
if support == "supported":
|
|
38
|
+
capability["probe"] = {
|
|
39
|
+
"required": True,
|
|
40
|
+
"command": list(probe_command or [host, "--help"]),
|
|
41
|
+
"liveCallsStarted": False,
|
|
42
|
+
}
|
|
43
|
+
capability["invocationContract"] = {
|
|
44
|
+
"requestSchema": "agent-host-operation-request.v1",
|
|
45
|
+
"receiptSchema": "agent-host-operation-receipt.v1",
|
|
46
|
+
"unsupportedOperationPolicy": "fail-closed",
|
|
47
|
+
}
|
|
48
|
+
return capability
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_host_capabilities(
|
|
52
|
+
capabilities: Any,
|
|
53
|
+
*,
|
|
54
|
+
adapter_id: str | None = None,
|
|
55
|
+
host: str | None = None,
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
"""Validate optional host capability declarations attached to a descriptor."""
|
|
58
|
+
|
|
59
|
+
blockers: list[dict[str, Any]] = []
|
|
60
|
+
if not isinstance(capabilities, list):
|
|
61
|
+
blockers.append({"code": "invalid-host-capabilities", "message": "hostCapabilities must be an array"})
|
|
62
|
+
return _validation_result(0, blockers)
|
|
63
|
+
for index, capability in enumerate(capabilities):
|
|
64
|
+
if not isinstance(capability, dict):
|
|
65
|
+
blockers.append({"code": "invalid-host-capability", "index": index, "message": "host capability must be an object"})
|
|
66
|
+
continue
|
|
67
|
+
_validate_capability_shape(capability, blockers, index=index, adapter_id=adapter_id, host=host)
|
|
68
|
+
return _validation_result(len(capabilities), blockers)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def validate_no_acp_evidence_for_hosts(capabilities: list[dict[str, Any]], *, excluded_hosts: set[str]) -> dict[str, Any]:
|
|
72
|
+
"""Validate that excluded hosts have no positive ACP evidence declarations."""
|
|
73
|
+
|
|
74
|
+
blockers: list[dict[str, Any]] = []
|
|
75
|
+
for capability in capabilities:
|
|
76
|
+
if capability.get("capabilityId") != ACP_CAPABILITY_ID:
|
|
77
|
+
continue
|
|
78
|
+
host = capability.get("host")
|
|
79
|
+
if isinstance(host, str) and host in excluded_hosts and capability.get("support") == "supported":
|
|
80
|
+
blockers.append({"code": "excluded-host-acp-evidence", "host": host})
|
|
81
|
+
return _validation_result(len(capabilities), blockers)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_acp_probe_receipt(
|
|
85
|
+
capability: dict[str, Any],
|
|
86
|
+
*,
|
|
87
|
+
executable_found: bool,
|
|
88
|
+
probe_passed: bool,
|
|
89
|
+
invocation_contract_valid: bool,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
"""Return a fail-closed ACP probe receipt without starting live model calls."""
|
|
92
|
+
|
|
93
|
+
validation = validate_host_capabilities(
|
|
94
|
+
[capability],
|
|
95
|
+
adapter_id=capability.get("adapterId") if isinstance(capability.get("adapterId"), str) else None,
|
|
96
|
+
host=capability.get("host") if isinstance(capability.get("host"), str) else None,
|
|
97
|
+
)
|
|
98
|
+
blockers = list(validation["blockers"])
|
|
99
|
+
support = capability.get("support")
|
|
100
|
+
checks = [
|
|
101
|
+
{"name": "capability-declaration", "status": validation["status"]},
|
|
102
|
+
{"name": "executable-present", "status": "PASS" if executable_found else "FAIL"},
|
|
103
|
+
{"name": "acp-probe", "status": "PASS" if probe_passed else "FAIL"},
|
|
104
|
+
{"name": "invocation-contract", "status": "PASS" if invocation_contract_valid else "FAIL"},
|
|
105
|
+
]
|
|
106
|
+
if support != "supported":
|
|
107
|
+
return {
|
|
108
|
+
"schemaVersion": ACP_PROBE_RECEIPT_SCHEMA_VERSION,
|
|
109
|
+
"status": "SKIPPED",
|
|
110
|
+
"adapterId": capability.get("adapterId"),
|
|
111
|
+
"host": capability.get("host"),
|
|
112
|
+
"capabilityId": ACP_CAPABILITY_ID,
|
|
113
|
+
"liveCallsStarted": False,
|
|
114
|
+
"checks": checks[:1],
|
|
115
|
+
"blockers": blockers,
|
|
116
|
+
}
|
|
117
|
+
if not executable_found:
|
|
118
|
+
blockers.append({"code": "acp-executable-missing"})
|
|
119
|
+
if not probe_passed:
|
|
120
|
+
blockers.append({"code": "acp-probe-failed"})
|
|
121
|
+
if not invocation_contract_valid:
|
|
122
|
+
blockers.append({"code": "acp-invocation-contract-invalid"})
|
|
123
|
+
return {
|
|
124
|
+
"schemaVersion": ACP_PROBE_RECEIPT_SCHEMA_VERSION,
|
|
125
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
126
|
+
"adapterId": capability.get("adapterId"),
|
|
127
|
+
"host": capability.get("host"),
|
|
128
|
+
"capabilityId": ACP_CAPABILITY_ID,
|
|
129
|
+
"liveCallsStarted": False,
|
|
130
|
+
"checks": checks,
|
|
131
|
+
"blockers": blockers,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def require_host_capabilities_pass(payload: dict[str, Any]) -> dict[str, Any]:
|
|
136
|
+
if payload.get("status") == "FAIL":
|
|
137
|
+
raise LifecycleError("host-capability-validation-failed", "host capability validation failed", {"validation": payload})
|
|
138
|
+
return payload
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _validate_capability_shape(
|
|
142
|
+
capability: dict[str, Any],
|
|
143
|
+
blockers: list[dict[str, Any]],
|
|
144
|
+
*,
|
|
145
|
+
index: int,
|
|
146
|
+
adapter_id: str | None,
|
|
147
|
+
host: str | None,
|
|
148
|
+
) -> None:
|
|
149
|
+
if capability.get("schemaVersion") != HOST_CAPABILITY_SCHEMA_VERSION:
|
|
150
|
+
blockers.append({"code": "invalid-host-capability-schema", "index": index})
|
|
151
|
+
if capability.get("capabilityId") != ACP_CAPABILITY_ID:
|
|
152
|
+
blockers.append({"code": "unsupported-host-capability", "index": index, "capabilityId": capability.get("capabilityId")})
|
|
153
|
+
if capability.get("support") not in _SUPPORTED_VALUES:
|
|
154
|
+
blockers.append({"code": "invalid-host-capability-support", "index": index})
|
|
155
|
+
if capability.get("providerIdentityUsed") is not False:
|
|
156
|
+
blockers.append({"code": "host-capability-provider-identity", "index": index})
|
|
157
|
+
if adapter_id is not None and capability.get("adapterId") != adapter_id:
|
|
158
|
+
blockers.append({"code": "host-capability-adapter-mismatch", "index": index})
|
|
159
|
+
if host is not None and capability.get("host") != host:
|
|
160
|
+
blockers.append({"code": "host-capability-host-mismatch", "index": index})
|
|
161
|
+
if capability.get("support") == "supported":
|
|
162
|
+
_validate_supported_acp(capability, blockers, index=index)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _validate_supported_acp(capability: dict[str, Any], blockers: list[dict[str, Any]], *, index: int) -> None:
|
|
166
|
+
if capability.get("transport") != "acp":
|
|
167
|
+
blockers.append({"code": "supported-acp-transport-mismatch", "index": index})
|
|
168
|
+
if capability.get("evidencePolicy") != "probe-required":
|
|
169
|
+
blockers.append({"code": "supported-acp-probe-policy-missing", "index": index})
|
|
170
|
+
probe = capability.get("probe")
|
|
171
|
+
if not isinstance(probe, dict) or probe.get("required") is not True:
|
|
172
|
+
blockers.append({"code": "supported-acp-probe-required", "index": index})
|
|
173
|
+
elif not isinstance(probe.get("command"), list) or not all(isinstance(item, str) and item for item in probe["command"]):
|
|
174
|
+
blockers.append({"code": "supported-acp-probe-command-invalid", "index": index})
|
|
175
|
+
elif probe.get("liveCallsStarted") is not False:
|
|
176
|
+
blockers.append({"code": "supported-acp-probe-live-call-overclaim", "index": index})
|
|
177
|
+
contract = capability.get("invocationContract")
|
|
178
|
+
if not isinstance(contract, dict):
|
|
179
|
+
blockers.append({"code": "supported-acp-invocation-contract-missing", "index": index})
|
|
180
|
+
return
|
|
181
|
+
if contract.get("requestSchema") != "agent-host-operation-request.v1":
|
|
182
|
+
blockers.append({"code": "supported-acp-request-schema", "index": index})
|
|
183
|
+
if contract.get("receiptSchema") != "agent-host-operation-receipt.v1":
|
|
184
|
+
blockers.append({"code": "supported-acp-receipt-schema", "index": index})
|
|
185
|
+
if contract.get("unsupportedOperationPolicy") != "fail-closed":
|
|
186
|
+
blockers.append({"code": "supported-acp-unsupported-policy", "index": index})
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _validation_result(capability_count: int, blockers: list[dict[str, Any]]) -> dict[str, Any]:
|
|
190
|
+
return {
|
|
191
|
+
"schemaVersion": HOST_CAPABILITY_VALIDATION_SCHEMA_VERSION,
|
|
192
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
193
|
+
"capabilityCount": capability_count,
|
|
194
|
+
"blockers": blockers,
|
|
195
|
+
}
|