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,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
|
+
)
|