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.
Files changed (227) hide show
  1. agent_lifecycle/__init__.py +5 -0
  2. agent_lifecycle/__main__.py +5 -0
  3. agent_lifecycle/_version.py +3 -0
  4. agent_lifecycle/adapter_sessions/__init__.py +41 -0
  5. agent_lifecycle/adapter_sessions/contracts.py +133 -0
  6. agent_lifecycle/adapter_sessions/env.py +44 -0
  7. agent_lifecycle/adapter_sessions/launcher.py +101 -0
  8. agent_lifecycle/adapter_sessions/process.py +39 -0
  9. agent_lifecycle/adapter_sessions/redaction.py +30 -0
  10. agent_lifecycle/adapter_sessions/session_store.py +64 -0
  11. agent_lifecycle/adapter_sessions/task_intake.py +450 -0
  12. agent_lifecycle/adapter_sessions/workflow_bridge.py +154 -0
  13. agent_lifecycle/audit/__init__.py +67 -0
  14. agent_lifecycle/audit/bug_forensics.py +94 -0
  15. agent_lifecycle/audit/implementation.py +516 -0
  16. agent_lifecycle/audit/ownership.py +119 -0
  17. agent_lifecycle/audit/proof_integrity.py +814 -0
  18. agent_lifecycle/audit/review_verdict.py +116 -0
  19. agent_lifecycle/changesets/__init__.py +5 -0
  20. agent_lifecycle/changesets/git.py +35 -0
  21. agent_lifecycle/cli/__init__.py +5 -0
  22. agent_lifecycle/cli/adapter.py +360 -0
  23. agent_lifecycle/cli/dispatch.py +1220 -0
  24. agent_lifecycle/cli/followup.py +89 -0
  25. agent_lifecycle/cli/main.py +41 -0
  26. agent_lifecycle/cli/metrics_parser.py +49 -0
  27. agent_lifecycle/cli/parsers.py +618 -0
  28. agent_lifecycle/cli/policy.py +101 -0
  29. agent_lifecycle/cli/progress_hooks.py +117 -0
  30. agent_lifecycle/cli/worktree.py +74 -0
  31. agent_lifecycle/compiler/__init__.py +17 -0
  32. agent_lifecycle/compiler/small_model_packets.py +322 -0
  33. agent_lifecycle/compiler/task_packets.py +229 -0
  34. agent_lifecycle/context/__init__.py +20 -0
  35. agent_lifecycle/context/episode_retrieval.py +30 -0
  36. agent_lifecycle/context/external_memory.py +71 -0
  37. agent_lifecycle/context/profiles.py +104 -0
  38. agent_lifecycle/context/rendering.py +226 -0
  39. agent_lifecycle/contracts/__init__.py +25 -0
  40. agent_lifecycle/contracts/adapter_contract_schemas.py +130 -0
  41. agent_lifecycle/contracts/adapter_event_schemas.py +118 -0
  42. agent_lifecycle/contracts/adapter_session_schemas.py +130 -0
  43. agent_lifecycle/contracts/adapter_task_schemas.py +90 -0
  44. agent_lifecycle/contracts/audit_schemas.py +133 -0
  45. agent_lifecycle/contracts/bug_forensics_schemas.py +400 -0
  46. agent_lifecycle/contracts/canonical.py +55 -0
  47. agent_lifecycle/contracts/compatibility.py +242 -0
  48. agent_lifecycle/contracts/context_model_schemas.py +265 -0
  49. agent_lifecycle/contracts/core_schemas.py +252 -0
  50. agent_lifecycle/contracts/cross_check_schemas.py +121 -0
  51. agent_lifecycle/contracts/errors.py +26 -0
  52. agent_lifecycle/contracts/evidence_import_schemas.py +152 -0
  53. agent_lifecycle/contracts/host_capability_schemas.py +64 -0
  54. agent_lifecycle/contracts/import_dialect_schemas.py +166 -0
  55. agent_lifecycle/contracts/metric_schemas.py +315 -0
  56. agent_lifecycle/contracts/paths.py +31 -0
  57. agent_lifecycle/contracts/plan_contract_schemas.py +148 -0
  58. agent_lifecycle/contracts/policy_schemas.py +350 -0
  59. agent_lifecycle/contracts/progress_bridge_schemas.py +114 -0
  60. agent_lifecycle/contracts/progress_hook_schemas.py +119 -0
  61. agent_lifecycle/contracts/proof_integrity_schemas.py +240 -0
  62. agent_lifecycle/contracts/release_contract_schemas.py +476 -0
  63. agent_lifecycle/contracts/review_mesh_recommendation_schemas.py +64 -0
  64. agent_lifecycle/contracts/review_mesh_schemas.py +222 -0
  65. agent_lifecycle/contracts/review_quality_schemas.py +109 -0
  66. agent_lifecycle/contracts/runner_schemas.py +91 -0
  67. agent_lifecycle/contracts/runner_worktree_schemas.py +389 -0
  68. agent_lifecycle/contracts/sandbox_schemas.py +159 -0
  69. agent_lifecycle/contracts/schema_builders.py +23 -0
  70. agent_lifecycle/contracts/schemas.py +93 -0
  71. agent_lifecycle/contracts/status_goal_schemas.py +469 -0
  72. agent_lifecycle/contracts/task_template_schemas.py +96 -0
  73. agent_lifecycle/contracts/usage_export_schemas.py +68 -0
  74. agent_lifecycle/diagnostics/__init__.py +17 -0
  75. agent_lifecycle/diagnostics/bundles.py +124 -0
  76. agent_lifecycle/diagnostics/readiness.py +733 -0
  77. agent_lifecycle/evidence_index/__init__.py +39 -0
  78. agent_lifecycle/evidence_index/core.py +272 -0
  79. agent_lifecycle/evidence_index/episode_index.py +329 -0
  80. agent_lifecycle/evidence_index/external_context.py +276 -0
  81. agent_lifecycle/followup/__init__.py +23 -0
  82. agent_lifecycle/followup/records.py +404 -0
  83. agent_lifecycle/freeze/__init__.py +5 -0
  84. agent_lifecycle/freeze/locks.py +23 -0
  85. agent_lifecycle/goal/__init__.py +15 -0
  86. agent_lifecycle/goal/records.py +244 -0
  87. agent_lifecycle/goal/view.py +109 -0
  88. agent_lifecycle/host_protocol/__init__.py +65 -0
  89. agent_lifecycle/host_protocol/acp_capability.py +195 -0
  90. agent_lifecycle/host_protocol/capabilities.py +268 -0
  91. agent_lifecycle/host_protocol/contracts.py +214 -0
  92. agent_lifecycle/host_protocol/event_capture.py +390 -0
  93. agent_lifecycle/host_protocol/events.py +196 -0
  94. agent_lifecycle/host_protocol/inspection.py +194 -0
  95. agent_lifecycle/host_protocol/inspection_common.py +310 -0
  96. agent_lifecycle/host_protocol/inspection_cursor.py +164 -0
  97. agent_lifecycle/host_protocol/inspection_gemini_cli.py +163 -0
  98. agent_lifecycle/host_protocol/inspection_hermes.py +161 -0
  99. agent_lifecycle/host_protocol/inspection_kimi_code.py +169 -0
  100. agent_lifecycle/host_protocol/inspection_opencode.py +147 -0
  101. agent_lifecycle/host_protocol/inspection_qwen_code.py +143 -0
  102. agent_lifecycle/host_protocol/receipts.py +58 -0
  103. agent_lifecycle/host_protocol/scaffold.py +329 -0
  104. agent_lifecycle/host_protocol/validation.py +229 -0
  105. agent_lifecycle/imports/__init__.py +76 -0
  106. agent_lifecycle/imports/agentskills_profile.py +54 -0
  107. agent_lifecycle/imports/bmad_profile.py +49 -0
  108. agent_lifecycle/imports/constitution_adr.py +91 -0
  109. agent_lifecycle/imports/dialect_profiles.py +121 -0
  110. agent_lifecycle/imports/external_agent.py +28 -0
  111. agent_lifecycle/imports/external_dialects.py +486 -0
  112. agent_lifecycle/imports/external_workflow.py +28 -0
  113. agent_lifecycle/imports/multi_markdown.py +111 -0
  114. agent_lifecycle/imports/openspec_profile.py +49 -0
  115. agent_lifecycle/imports/planning.py +536 -0
  116. agent_lifecycle/imports/spec_kit_profile.py +49 -0
  117. agent_lifecycle/imports/spec_kitty_profile.py +49 -0
  118. agent_lifecycle/metrics/__init__.py +60 -0
  119. agent_lifecycle/metrics/cost_collection.py +315 -0
  120. agent_lifecycle/metrics/costs.py +203 -0
  121. agent_lifecycle/metrics/outcome_index.py +330 -0
  122. agent_lifecycle/metrics/phase_resources.py +208 -0
  123. agent_lifecycle/metrics/recommendations.py +507 -0
  124. agent_lifecycle/metrics/regression_signals.py +64 -0
  125. agent_lifecycle/metrics/usage_export.py +390 -0
  126. agent_lifecycle/model_routing/__init__.py +19 -0
  127. agent_lifecycle/model_routing/profiles.py +323 -0
  128. agent_lifecycle/model_routing/receipts.py +140 -0
  129. agent_lifecycle/model_routing/resolver.py +566 -0
  130. agent_lifecycle/neutrality/__init__.py +12 -0
  131. agent_lifecycle/neutrality/__main__.py +6 -0
  132. agent_lifecycle/neutrality/authority.py +265 -0
  133. agent_lifecycle/neutrality/canonical.py +28 -0
  134. agent_lifecycle/neutrality/cli.py +294 -0
  135. agent_lifecycle/neutrality/ed25519.py +124 -0
  136. agent_lifecycle/neutrality/errors.py +89 -0
  137. agent_lifecycle/neutrality/gate.py +269 -0
  138. agent_lifecycle/neutrality/paths.py +92 -0
  139. agent_lifecycle/neutrality/policy.py +76 -0
  140. agent_lifecycle/neutrality/receipt.py +135 -0
  141. agent_lifecycle/neutrality/scanner.py +310 -0
  142. agent_lifecycle/neutrality/test_runner.py +116 -0
  143. agent_lifecycle/planning/__init__.py +47 -0
  144. agent_lifecycle/planning/acceptance_markdown.py +159 -0
  145. agent_lifecycle/planning/completeness.py +362 -0
  146. agent_lifecycle/planning/continuity.py +255 -0
  147. agent_lifecycle/planning/templates.py +294 -0
  148. agent_lifecycle/planning/tiering.py +87 -0
  149. agent_lifecycle/planning/validation.py +72 -0
  150. agent_lifecycle/policy/__init__.py +37 -0
  151. agent_lifecycle/policy/adaptive_lifecycle.py +416 -0
  152. agent_lifecycle/policy/apply.py +44 -0
  153. agent_lifecycle/policy/proposals.py +189 -0
  154. agent_lifecycle/policy/quality_floor.py +144 -0
  155. agent_lifecycle/policy/runtime_receipts.py +160 -0
  156. agent_lifecycle/py.typed +1 -0
  157. agent_lifecycle/quality/__init__.py +89 -0
  158. agent_lifecycle/quality/bug_forensics.py +727 -0
  159. agent_lifecycle/quality/bug_forensics_advisor.py +88 -0
  160. agent_lifecycle/quality/bug_forensics_recipes.py +239 -0
  161. agent_lifecycle/quality/cross_check.py +437 -0
  162. agent_lifecycle/quality/failure_classification.py +318 -0
  163. agent_lifecycle/quality/packs.py +265 -0
  164. agent_lifecycle/reporting/__init__.py +37 -0
  165. agent_lifecycle/reporting/change_summary.py +156 -0
  166. agent_lifecycle/reporting/event_feed.py +101 -0
  167. agent_lifecycle/reporting/progress_bridge.py +153 -0
  168. agent_lifecycle/reporting/progress_hooks.py +161 -0
  169. agent_lifecycle/reporting/progress_terminal.py +95 -0
  170. agent_lifecycle/reporting/progress_view.py +284 -0
  171. agent_lifecycle/reporting/status_view.py +125 -0
  172. agent_lifecycle/reporting/usage_export.py +59 -0
  173. agent_lifecycle/review/__init__.py +5 -0
  174. agent_lifecycle/review/validation.py +29 -0
  175. agent_lifecycle/review_mesh/__init__.py +83 -0
  176. agent_lifecycle/review_mesh/assignments.py +160 -0
  177. agent_lifecycle/review_mesh/contracts.py +685 -0
  178. agent_lifecycle/review_mesh/operator_templates.py +320 -0
  179. agent_lifecycle/review_mesh/quorum.py +38 -0
  180. agent_lifecycle/review_mesh/recommendation.py +487 -0
  181. agent_lifecycle/review_mesh/results.py +98 -0
  182. agent_lifecycle/review_mesh/synthesis.py +102 -0
  183. agent_lifecycle/runner/__init__.py +47 -0
  184. agent_lifecycle/runner/attempt_snapshots.py +244 -0
  185. agent_lifecycle/runner/core.py +653 -0
  186. agent_lifecycle/runner/sandbox_receipts.py +628 -0
  187. agent_lifecycle/specification/__init__.py +23 -0
  188. agent_lifecycle/specification/completion_check.py +135 -0
  189. agent_lifecycle/specification/completion_gate.py +501 -0
  190. agent_lifecycle/specification/completion_signal.py +75 -0
  191. agent_lifecycle/specification/validation.py +48 -0
  192. agent_lifecycle/workflow/__init__.py +79 -0
  193. agent_lifecycle/workflow/artifacts.py +74 -0
  194. agent_lifecycle/workflow/budget_decisions.py +370 -0
  195. agent_lifecycle/workflow/budget_policy.py +140 -0
  196. agent_lifecycle/workflow/budget_receipts.py +70 -0
  197. agent_lifecycle/workflow/bug_forensics_gates.py +284 -0
  198. agent_lifecycle/workflow/controller.py +45 -0
  199. agent_lifecycle/workflow/events.py +50 -0
  200. agent_lifecycle/workflow/final_proof_integrity.py +74 -0
  201. agent_lifecycle/workflow/finalization.py +422 -0
  202. agent_lifecycle/workflow/gates.py +185 -0
  203. agent_lifecycle/workflow/implementation_audit_gate.py +126 -0
  204. agent_lifecycle/workflow/leases.py +220 -0
  205. agent_lifecycle/workflow/lineage.py +108 -0
  206. agent_lifecycle/workflow/managed_runner.py +167 -0
  207. agent_lifecycle/workflow/model_usage.py +106 -0
  208. agent_lifecycle/workflow/next_action.py +86 -0
  209. agent_lifecycle/workflow/operation_kernel.py +98 -0
  210. agent_lifecycle/workflow/plan_adoption.py +439 -0
  211. agent_lifecycle/workflow/query.py +52 -0
  212. agent_lifecycle/workflow/review_mesh_gate.py +93 -0
  213. agent_lifecycle/workflow/reviews.py +168 -0
  214. agent_lifecycle/workflow/run_transitions.py +163 -0
  215. agent_lifecycle/workflow/sandbox_policy.py +214 -0
  216. agent_lifecycle/workflow/selectors.py +57 -0
  217. agent_lifecycle/workflow/state.py +130 -0
  218. agent_lifecycle/workflow/task_transitions.py +421 -0
  219. agent_lifecycle/worktree/__init__.py +17 -0
  220. agent_lifecycle/worktree/isolation.py +407 -0
  221. agent_lifecycle_kit-1.50.1.dist-info/METADATA +201 -0
  222. agent_lifecycle_kit-1.50.1.dist-info/RECORD +227 -0
  223. agent_lifecycle_kit-1.50.1.dist-info/WHEEL +5 -0
  224. agent_lifecycle_kit-1.50.1.dist-info/entry_points.txt +3 -0
  225. agent_lifecycle_kit-1.50.1.dist-info/licenses/LICENSE +201 -0
  226. agent_lifecycle_kit-1.50.1.dist-info/licenses/NOTICE +5 -0
  227. agent_lifecycle_kit-1.50.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,268 @@
1
+ """Adapter capability manifests derived from host descriptors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import canonical_digest
8
+ from agent_lifecycle.host_protocol.acp_capability import validate_host_capabilities
9
+ from agent_lifecycle.host_protocol.event_capture import EVENT_CAPTURE_OPERATION, adapter_declares_event_capture, event_capture_declaration
10
+ from agent_lifecycle.host_protocol.validation import REQUIRED_OPERATION_NAMES, validate_adapter_descriptor
11
+ from agent_lifecycle.runner import validate_sandbox_capability
12
+
13
+ CAPABILITY_MANIFEST_SCHEMA_VERSION = "agent-adapter-capability-manifest.v1"
14
+ CAPABILITY_MANIFEST_VALIDATION_SCHEMA_VERSION = "agent-adapter-capability-manifest-validation.v1"
15
+
16
+
17
+ def build_capability_manifest(descriptor: dict[str, Any]) -> dict[str, Any]:
18
+ """Build a stable capability manifest from an adapter descriptor."""
19
+
20
+ model_routing = descriptor.get("modelRouting") if isinstance(descriptor.get("modelRouting"), dict) else {}
21
+ operations = descriptor.get("operations") if isinstance(descriptor.get("operations"), list) else []
22
+ promotion = {
23
+ "verifiedRequiresLiveTestedHostRange": True,
24
+ "productionPromotionClaimed": False,
25
+ }
26
+ live_range = descriptor.get("liveTestedHostRange")
27
+ if isinstance(live_range, dict):
28
+ promotion["liveTestedHostRange"] = live_range
29
+ return {
30
+ "schemaVersion": CAPABILITY_MANIFEST_SCHEMA_VERSION,
31
+ "adapterId": descriptor.get("adapterId"),
32
+ "host": descriptor.get("host"),
33
+ "maturity": descriptor.get("maturity"),
34
+ "descriptorDigest": canonical_digest(descriptor),
35
+ "unsupportedOperationPolicy": descriptor.get("unsupportedOperationPolicy"),
36
+ "coreSemantics": descriptor.get("coreSemantics"),
37
+ "capabilities": [_capability_from_operation(item, descriptor) for item in operations if isinstance(item, dict)],
38
+ "modelRouting": {
39
+ "profileSupport": model_routing.get("profileSupport"),
40
+ "attemptRoutePolicy": model_routing.get("attemptRoutePolicy"),
41
+ "usageReceiptRequired": model_routing.get("usageReceiptRequired"),
42
+ "unsupportedClassPolicy": model_routing.get("unsupportedClassPolicy"),
43
+ "providerModelNamesInCore": model_routing.get("providerModelNamesInCore"),
44
+ "liveVerified": model_routing.get("liveVerified"),
45
+ },
46
+ "runtimeBoundary": {
47
+ "hostSpecificCode": "adapter-owned",
48
+ "lifecycleSemantics": descriptor.get("coreSemantics"),
49
+ "unsupportedOperations": descriptor.get("unsupportedOperationPolicy"),
50
+ "providerModelNamesInCore": model_routing.get("providerModelNamesInCore"),
51
+ },
52
+ "hostCapabilities": _host_capabilities_from_descriptor(descriptor),
53
+ "sandboxCapabilities": _sandbox_capabilities_from_descriptor(descriptor),
54
+ "eventCapture": _event_capture_from_descriptor(descriptor),
55
+ "promotion": promotion,
56
+ }
57
+
58
+
59
+ def validate_capability_manifest(manifest: dict[str, Any], *, descriptor: dict[str, Any]) -> dict[str, Any]:
60
+ """Validate that a capability manifest still matches its descriptor."""
61
+
62
+ blockers: list[dict[str, Any]] = []
63
+ descriptor_validation = validate_adapter_descriptor(descriptor)
64
+ if descriptor_validation["status"] == "FAIL":
65
+ blockers.append(
66
+ {
67
+ "code": "adapter-descriptor-invalid",
68
+ "message": "capability manifest cannot pass while descriptor validation fails",
69
+ "descriptorBlockers": descriptor_validation["blockers"],
70
+ }
71
+ )
72
+ if manifest.get("schemaVersion") != CAPABILITY_MANIFEST_SCHEMA_VERSION:
73
+ blockers.append({"code": "invalid-capability-manifest-schema", "message": "unsupported capability manifest schemaVersion"})
74
+ for key in ("adapterId", "host", "maturity", "unsupportedOperationPolicy", "coreSemantics"):
75
+ if manifest.get(key) != descriptor.get(key):
76
+ blockers.append(
77
+ {
78
+ "code": "capability-manifest-descriptor-mismatch",
79
+ "field": key,
80
+ "expected": descriptor.get(key),
81
+ "actual": manifest.get(key),
82
+ }
83
+ )
84
+ expected_digest = canonical_digest(descriptor)
85
+ if manifest.get("descriptorDigest") != expected_digest:
86
+ blockers.append(
87
+ {
88
+ "code": "capability-manifest-descriptor-digest-mismatch",
89
+ "expected": expected_digest,
90
+ "actual": manifest.get("descriptorDigest"),
91
+ }
92
+ )
93
+ _validate_manifest_capabilities(manifest, descriptor, blockers)
94
+ _validate_runtime_boundary(manifest, descriptor, blockers)
95
+ _validate_host_capability_drift(manifest, descriptor, blockers)
96
+ _validate_sandbox_capability_drift(manifest, descriptor, blockers)
97
+ _validate_event_capture(manifest, descriptor, blockers)
98
+ status = "PASS" if not blockers else "FAIL"
99
+ return {
100
+ "schemaVersion": CAPABILITY_MANIFEST_VALIDATION_SCHEMA_VERSION,
101
+ "status": status,
102
+ "adapterId": descriptor.get("adapterId"),
103
+ "host": descriptor.get("host"),
104
+ "maturity": descriptor.get("maturity"),
105
+ "capabilityCount": len(manifest.get("capabilities", [])) if isinstance(manifest.get("capabilities"), list) else 0,
106
+ "blockers": blockers,
107
+ }
108
+
109
+
110
+ def _capability_from_operation(operation: dict[str, Any], descriptor: dict[str, Any]) -> dict[str, Any]:
111
+ offline_conformance = operation.get("offlineConformance")
112
+ return {
113
+ "name": operation.get("name"),
114
+ "mapping": operation.get("mapping"),
115
+ "offlineConformance": offline_conformance,
116
+ "support": "declared",
117
+ "unsupportedOperationPolicy": descriptor.get("unsupportedOperationPolicy"),
118
+ "lifecycleSemantics": descriptor.get("coreSemantics"),
119
+ "liveEvidenceRequiredForVerified": offline_conformance != "deterministic",
120
+ }
121
+
122
+
123
+ def _event_capture_from_descriptor(descriptor: dict[str, Any]) -> dict[str, Any]:
124
+ if adapter_declares_event_capture(descriptor=descriptor):
125
+ return event_capture_declaration()
126
+ return {
127
+ "status": "NOT_DECLARED",
128
+ "portableEventSchema": "agent-adapter-event.v1",
129
+ "categories": [],
130
+ "producerBoundary": None,
131
+ "promotionRequired": False,
132
+ }
133
+
134
+
135
+ def _host_capabilities_from_descriptor(descriptor: dict[str, Any]) -> list[dict[str, Any]]:
136
+ capabilities = descriptor.get("hostCapabilities")
137
+ if not isinstance(capabilities, list):
138
+ return []
139
+ return [dict(item) for item in capabilities if isinstance(item, dict)]
140
+
141
+
142
+ def _sandbox_capabilities_from_descriptor(descriptor: dict[str, Any]) -> dict[str, Any] | None:
143
+ capabilities = descriptor.get("sandboxCapabilities")
144
+ if not isinstance(capabilities, dict):
145
+ return None
146
+ return dict(capabilities)
147
+
148
+
149
+ def _validate_manifest_capabilities(
150
+ manifest: dict[str, Any],
151
+ descriptor: dict[str, Any],
152
+ blockers: list[dict[str, Any]],
153
+ ) -> None:
154
+ capabilities = manifest.get("capabilities")
155
+ if not isinstance(capabilities, list):
156
+ blockers.append({"code": "invalid-capability-manifest", "message": "capabilities must be an array"})
157
+ return
158
+ descriptor_operations = descriptor.get("operations")
159
+ if not isinstance(descriptor_operations, list):
160
+ return
161
+ descriptor_names = {item.get("name") for item in descriptor_operations if isinstance(item, dict)}
162
+ manifest_names = {item.get("name") for item in capabilities if isinstance(item, dict)}
163
+ missing_required = sorted(REQUIRED_OPERATION_NAMES.difference(manifest_names))
164
+ extra = sorted(manifest_names.difference(descriptor_names))
165
+ missing_descriptor = sorted(descriptor_names.difference(manifest_names))
166
+ if missing_required:
167
+ blockers.append({"code": "capability-required-operation-missing", "operations": missing_required})
168
+ if missing_descriptor or extra:
169
+ blockers.append(
170
+ {
171
+ "code": "capability-manifest-operation-drift",
172
+ "missingFromManifest": missing_descriptor,
173
+ "extraInManifest": extra,
174
+ }
175
+ )
176
+ for item in capabilities:
177
+ if not isinstance(item, dict):
178
+ blockers.append({"code": "invalid-capability-entry", "message": "capability entries must be objects"})
179
+ continue
180
+ if item.get("unsupportedOperationPolicy") != "fail-closed":
181
+ blockers.append({"code": "capability-unsupported-operation-policy", "capability": item.get("name")})
182
+ if item.get("lifecycleSemantics") != "delegated-to-agent-lifecycle-core":
183
+ blockers.append({"code": "capability-core-semantics-overclaim", "capability": item.get("name")})
184
+
185
+
186
+ def _validate_runtime_boundary(
187
+ manifest: dict[str, Any],
188
+ descriptor: dict[str, Any],
189
+ blockers: list[dict[str, Any]],
190
+ ) -> None:
191
+ boundary = manifest.get("runtimeBoundary")
192
+ if not isinstance(boundary, dict):
193
+ blockers.append({"code": "capability-runtime-boundary-missing", "message": "runtimeBoundary must be an object"})
194
+ return
195
+ if boundary.get("lifecycleSemantics") != descriptor.get("coreSemantics"):
196
+ blockers.append({"code": "capability-runtime-boundary-drift", "field": "lifecycleSemantics"})
197
+ if boundary.get("unsupportedOperations") != "fail-closed":
198
+ blockers.append({"code": "capability-runtime-boundary-policy", "field": "unsupportedOperations"})
199
+ if boundary.get("providerModelNamesInCore") is not False:
200
+ blockers.append({"code": "capability-provider-model-boundary", "message": "provider model names must stay out of core contracts"})
201
+
202
+
203
+ def _validate_host_capability_drift(
204
+ manifest: dict[str, Any],
205
+ descriptor: dict[str, Any],
206
+ blockers: list[dict[str, Any]],
207
+ ) -> None:
208
+ descriptor_capabilities = descriptor.get("hostCapabilities")
209
+ manifest_capabilities = manifest.get("hostCapabilities")
210
+ if descriptor_capabilities is None and manifest_capabilities is None:
211
+ return
212
+ if not isinstance(descriptor_capabilities, list):
213
+ descriptor_capabilities = []
214
+ if manifest_capabilities != descriptor_capabilities:
215
+ blockers.append({"code": "capability-manifest-host-capability-drift"})
216
+ return
217
+ validation = validate_host_capabilities(
218
+ manifest_capabilities,
219
+ adapter_id=descriptor.get("adapterId") if isinstance(descriptor.get("adapterId"), str) else None,
220
+ host=descriptor.get("host") if isinstance(descriptor.get("host"), str) else None,
221
+ )
222
+ if validation["status"] == "FAIL":
223
+ blockers.append({"code": "capability-manifest-host-capability-invalid", "blockers": validation["blockers"]})
224
+
225
+
226
+ def _validate_sandbox_capability_drift(
227
+ manifest: dict[str, Any],
228
+ descriptor: dict[str, Any],
229
+ blockers: list[dict[str, Any]],
230
+ ) -> None:
231
+ descriptor_capability = descriptor.get("sandboxCapabilities")
232
+ manifest_capability = manifest.get("sandboxCapabilities")
233
+ if descriptor_capability is None and manifest_capability is None:
234
+ return
235
+ if manifest_capability != descriptor_capability:
236
+ blockers.append({"code": "capability-manifest-sandbox-capability-drift"})
237
+ return
238
+ if not isinstance(manifest_capability, dict):
239
+ blockers.append({"code": "capability-manifest-sandbox-capability-invalid"})
240
+ return
241
+ validation = validate_sandbox_capability(manifest_capability)
242
+ if validation["status"] == "FAIL":
243
+ blockers.append({"code": "capability-manifest-sandbox-capability-invalid", "blockers": validation["blockers"]})
244
+
245
+
246
+ def _validate_event_capture(
247
+ manifest: dict[str, Any],
248
+ descriptor: dict[str, Any],
249
+ blockers: list[dict[str, Any]],
250
+ ) -> None:
251
+ capture = manifest.get("eventCapture")
252
+ if not isinstance(capture, dict):
253
+ blockers.append({"code": "capability-event-capture-missing", "message": "eventCapture must be an object"})
254
+ return
255
+ expected_declared = adapter_declares_event_capture(descriptor=descriptor)
256
+ actual_declared = capture.get("status") == "DECLARED"
257
+ if actual_declared != expected_declared:
258
+ blockers.append({"code": "capability-event-capture-drift", "expectedDeclared": expected_declared, "actualStatus": capture.get("status")})
259
+ if actual_declared:
260
+ operations = descriptor.get("operations", [])
261
+ if not any(isinstance(item, dict) and item.get("name") == EVENT_CAPTURE_OPERATION for item in operations):
262
+ blockers.append({"code": "capability-event-capture-operation-missing", "operation": EVENT_CAPTURE_OPERATION})
263
+ if capture.get("portableEventSchema") != "agent-adapter-event.v1":
264
+ blockers.append({"code": "capability-event-capture-schema", "message": "event capture must use agent-adapter-event.v1"})
265
+ if capture.get("producerBoundary") != "adapter-owned":
266
+ blockers.append({"code": "capability-event-capture-boundary", "message": "event producer boundary must be adapter-owned"})
267
+ if capture.get("promotionRequired") is not False:
268
+ blockers.append({"code": "capability-event-capture-promotion-overclaim", "message": "event capture declaration must not imply promotion"})
@@ -0,0 +1,214 @@
1
+ """Closed host-operation envelopes used by adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Literal
7
+
8
+ from agent_lifecycle.contracts.errors import LifecycleError
9
+
10
+ HostOperationStatus = Literal["PASS", "FAIL", "BLOCKED"]
11
+ HostAdapterEventStatus = Literal["INFO", "PASS", "FAIL", "BLOCKED"]
12
+ HostAdapterEventType = Literal[
13
+ "session.started",
14
+ "task.launched",
15
+ "command.completed",
16
+ "writes.summarized",
17
+ "usage.reported",
18
+ "task.blocked",
19
+ "task.completed",
20
+ ]
21
+
22
+
23
+ def _reject_unknown(value: dict[str, Any], allowed: set[str], label: str) -> None:
24
+ unknown = sorted(set(value).difference(allowed))
25
+ if unknown:
26
+ raise LifecycleError("unknown-field", f"{label}: unknown fields: {', '.join(unknown)}")
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class HostOperationRequest:
31
+ operation_id: str
32
+ capability: str
33
+ inputs: dict[str, Any] = field(default_factory=dict)
34
+ outputs: list[dict[str, Any]] = field(default_factory=list)
35
+ constraints: dict[str, Any] = field(default_factory=dict)
36
+ model_route: dict[str, Any] | None = None
37
+
38
+ schema_version = "agent-host-operation-request.v1"
39
+
40
+ def to_json(self) -> dict[str, Any]:
41
+ payload = {
42
+ "schemaVersion": self.schema_version,
43
+ "operationId": self.operation_id,
44
+ "capability": self.capability,
45
+ "inputs": self.inputs,
46
+ "outputs": self.outputs,
47
+ "constraints": self.constraints,
48
+ }
49
+ if self.model_route is not None:
50
+ payload["modelRoute"] = self.model_route
51
+ return payload
52
+
53
+ @classmethod
54
+ def from_json(cls, value: dict[str, Any]) -> "HostOperationRequest":
55
+ allowed = {"schemaVersion", "operationId", "capability", "inputs", "outputs", "constraints", "modelRoute"}
56
+ _reject_unknown(value, allowed, "host operation request")
57
+ if value.get("schemaVersion") != cls.schema_version:
58
+ raise LifecycleError("unsupported-schema", "host operation request schemaVersion is unsupported")
59
+ if not isinstance(value.get("operationId"), str) or not value["operationId"]:
60
+ raise LifecycleError("invalid-host-operation", "operationId is required")
61
+ if not isinstance(value.get("capability"), str) or not value["capability"]:
62
+ raise LifecycleError("invalid-host-operation", "capability is required")
63
+ if not isinstance(value.get("inputs"), dict):
64
+ raise LifecycleError("invalid-host-operation", "inputs must be an object")
65
+ if not isinstance(value.get("outputs"), list):
66
+ raise LifecycleError("invalid-host-operation", "outputs must be an array")
67
+ if not isinstance(value.get("constraints"), dict):
68
+ raise LifecycleError("invalid-host-operation", "constraints must be an object")
69
+ model_route = value.get("modelRoute")
70
+ if model_route is not None and not isinstance(model_route, dict):
71
+ raise LifecycleError("invalid-host-operation", "modelRoute must be an object")
72
+ return cls(
73
+ operation_id=value["operationId"],
74
+ capability=value["capability"],
75
+ inputs=value["inputs"],
76
+ outputs=value["outputs"],
77
+ constraints=value["constraints"],
78
+ model_route=model_route,
79
+ )
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class HostOperationReceipt:
84
+ operation_id: str
85
+ capability: str
86
+ status: HostOperationStatus
87
+ outputs: list[dict[str, Any]] = field(default_factory=list)
88
+ usage: dict[str, Any] = field(default_factory=dict)
89
+
90
+ schema_version = "agent-host-operation-receipt.v1"
91
+
92
+ def to_json(self) -> dict[str, Any]:
93
+ return {
94
+ "schemaVersion": self.schema_version,
95
+ "operationId": self.operation_id,
96
+ "capability": self.capability,
97
+ "status": self.status,
98
+ "outputs": self.outputs,
99
+ "usage": self.usage,
100
+ }
101
+
102
+ @classmethod
103
+ def from_json(cls, value: dict[str, Any]) -> "HostOperationReceipt":
104
+ allowed = {"schemaVersion", "operationId", "capability", "status", "outputs", "usage"}
105
+ _reject_unknown(value, allowed, "host operation receipt")
106
+ if value.get("schemaVersion") != cls.schema_version:
107
+ raise LifecycleError("unsupported-schema", "host operation receipt schemaVersion is unsupported")
108
+ status = value.get("status")
109
+ if status not in {"PASS", "FAIL", "BLOCKED"}:
110
+ raise LifecycleError("invalid-host-operation", "status is unsupported")
111
+ if not isinstance(value.get("operationId"), str) or not value["operationId"]:
112
+ raise LifecycleError("invalid-host-operation", "operationId is required")
113
+ if not isinstance(value.get("capability"), str) or not value["capability"]:
114
+ raise LifecycleError("invalid-host-operation", "capability is required")
115
+ if not isinstance(value.get("outputs"), list):
116
+ raise LifecycleError("invalid-host-operation", "outputs must be an array")
117
+ if not isinstance(value.get("usage"), dict):
118
+ raise LifecycleError("invalid-host-operation", "usage must be an object")
119
+ return cls(
120
+ operation_id=value["operationId"],
121
+ capability=value["capability"],
122
+ status=status,
123
+ outputs=value["outputs"],
124
+ usage=value["usage"],
125
+ )
126
+
127
+
128
+ @dataclass(frozen=True, slots=True)
129
+ class HostAdapterEvent:
130
+ event_id: str
131
+ host: str
132
+ adapter_id: str
133
+ run_id: str
134
+ task_id: str
135
+ operation_id: str
136
+ sequence: int
137
+ event_type: HostAdapterEventType
138
+ status: HostAdapterEventStatus
139
+ recorded_at: str
140
+ payload: dict[str, Any] = field(default_factory=dict)
141
+
142
+ schema_version = "agent-adapter-event.v1"
143
+
144
+ def to_json(self) -> dict[str, Any]:
145
+ return {
146
+ "schemaVersion": self.schema_version,
147
+ "eventId": self.event_id,
148
+ "host": self.host,
149
+ "adapterId": self.adapter_id,
150
+ "runId": self.run_id,
151
+ "taskId": self.task_id,
152
+ "operationId": self.operation_id,
153
+ "sequence": self.sequence,
154
+ "eventType": self.event_type,
155
+ "status": self.status,
156
+ "recordedAt": self.recorded_at,
157
+ "payload": self.payload,
158
+ }
159
+
160
+ @classmethod
161
+ def from_json(cls, value: dict[str, Any]) -> "HostAdapterEvent":
162
+ allowed = {
163
+ "schemaVersion",
164
+ "eventId",
165
+ "host",
166
+ "adapterId",
167
+ "runId",
168
+ "taskId",
169
+ "operationId",
170
+ "sequence",
171
+ "eventType",
172
+ "status",
173
+ "recordedAt",
174
+ "payload",
175
+ }
176
+ _reject_unknown(value, allowed, "adapter event")
177
+ if value.get("schemaVersion") != cls.schema_version:
178
+ raise LifecycleError("unsupported-schema", "adapter event schemaVersion is unsupported")
179
+ for key in ("eventId", "host", "adapterId", "runId", "taskId", "operationId", "recordedAt"):
180
+ if not isinstance(value.get(key), str) or not value[key]:
181
+ raise LifecycleError("invalid-adapter-event", f"{key} is required")
182
+ sequence = value.get("sequence")
183
+ if not isinstance(sequence, int) or isinstance(sequence, bool) or sequence < 1:
184
+ raise LifecycleError("invalid-adapter-event", "sequence must be a positive integer")
185
+ event_type = value.get("eventType")
186
+ if event_type not in {
187
+ "session.started",
188
+ "task.launched",
189
+ "command.completed",
190
+ "writes.summarized",
191
+ "usage.reported",
192
+ "task.blocked",
193
+ "task.completed",
194
+ }:
195
+ raise LifecycleError("invalid-adapter-event", "eventType is unsupported")
196
+ status = value.get("status")
197
+ if status not in {"INFO", "PASS", "FAIL", "BLOCKED"}:
198
+ raise LifecycleError("invalid-adapter-event", "status is unsupported")
199
+ payload = value.get("payload")
200
+ if not isinstance(payload, dict):
201
+ raise LifecycleError("invalid-adapter-event", "payload must be an object")
202
+ return cls(
203
+ event_id=value["eventId"],
204
+ host=value["host"],
205
+ adapter_id=value["adapterId"],
206
+ run_id=value["runId"],
207
+ task_id=value["taskId"],
208
+ operation_id=value["operationId"],
209
+ sequence=sequence,
210
+ event_type=event_type,
211
+ status=status,
212
+ recorded_at=value["recordedAt"],
213
+ payload=payload,
214
+ )