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,135 @@
|
|
|
1
|
+
"""Detached neutrality receipt generation and verification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .authority import RECEIPT_DOMAIN
|
|
9
|
+
from .canonical import canonical_bytes, load_json, sha256_hex
|
|
10
|
+
from .ed25519 import fingerprint, verify
|
|
11
|
+
from .errors import NeutralityError
|
|
12
|
+
from .paths import stable_read_bytes
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_claims(
|
|
16
|
+
*,
|
|
17
|
+
operation: dict[str, Any],
|
|
18
|
+
report: dict[str, Any],
|
|
19
|
+
authority_digest: str,
|
|
20
|
+
primary_path: str,
|
|
21
|
+
receipt_path: str,
|
|
22
|
+
policy: dict[str, Any],
|
|
23
|
+
profile: dict[str, Any],
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
zero_counters = {
|
|
26
|
+
key: int(report["counters"].get(key, 0))
|
|
27
|
+
for key in [
|
|
28
|
+
"findings",
|
|
29
|
+
"skippedInputs",
|
|
30
|
+
"opaqueInputs",
|
|
31
|
+
"readRaces",
|
|
32
|
+
"incompleteScans",
|
|
33
|
+
"unsupportedArchives",
|
|
34
|
+
"archiveLimitBreaches",
|
|
35
|
+
"occupiedOutputConflicts",
|
|
36
|
+
"pathAliasConflicts",
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
output_manifest = [
|
|
40
|
+
{
|
|
41
|
+
"publicationOrder": 1,
|
|
42
|
+
"role": "primary-artifact",
|
|
43
|
+
"path": primary_path,
|
|
44
|
+
"required": True,
|
|
45
|
+
"mustBeUnique": True,
|
|
46
|
+
"contentIdentityRule": "sha256",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"publicationOrder": 2,
|
|
50
|
+
"role": "detached-commit-receipt",
|
|
51
|
+
"path": receipt_path,
|
|
52
|
+
"required": True,
|
|
53
|
+
"mustBeUnique": True,
|
|
54
|
+
"contentIdentityRule": "canonical-signed-envelope",
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
return {
|
|
58
|
+
"schemaVersion": "agent-neutrality-claims.v3",
|
|
59
|
+
"operation": operation,
|
|
60
|
+
"projectionSchemaVersion": "agent-neutrality-subject-projection.v2",
|
|
61
|
+
"profileDigest": sha256_hex(canonical_bytes(profile)),
|
|
62
|
+
"scopeProfileDigest": sha256_hex(canonical_bytes({"scope": report["scope"]})),
|
|
63
|
+
"archivePolicyDigest": sha256_hex(canonical_bytes(policy.get("archives", {}))),
|
|
64
|
+
"externalAuthorityContractDigest": sha256_hex(canonical_bytes({"contract": "AGENT_LIFECYCLE_NEUTRALITY_*"})),
|
|
65
|
+
"operationOutputManifestDigest": sha256_hex(canonical_bytes(output_manifest)),
|
|
66
|
+
"primaryArtifactManifestDigest": sha256_hex(canonical_bytes({"primary": primary_path})),
|
|
67
|
+
"workingTreeDigest": report["digests"]["workingTreeDigest"],
|
|
68
|
+
"gitObjectSetDigest": report["digests"]["gitObjectSetDigest"],
|
|
69
|
+
"subjectDigest": report["digests"]["subjectDigest"],
|
|
70
|
+
"authorityDigest": authority_digest,
|
|
71
|
+
"zeroCounters": zero_counters,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_receipt(
|
|
76
|
+
*,
|
|
77
|
+
operation: dict[str, Any],
|
|
78
|
+
claims: dict[str, Any],
|
|
79
|
+
primary_sha256: str,
|
|
80
|
+
primary_bytes: int,
|
|
81
|
+
signer_fingerprint: str,
|
|
82
|
+
signature: str,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
claims_digest = sha256_hex(canonical_bytes(claims))
|
|
85
|
+
return {
|
|
86
|
+
"schemaVersion": "agent-neutrality-detached-receipt.v3",
|
|
87
|
+
"operation": operation,
|
|
88
|
+
"claimsDigest": claims_digest,
|
|
89
|
+
"primaryArtifact": {"sha256": primary_sha256, "bytes": primary_bytes},
|
|
90
|
+
"signer": {"algorithm": "Ed25519", "fingerprint": signer_fingerprint},
|
|
91
|
+
"signature": signature,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def verify_existing_receipt(
|
|
96
|
+
*,
|
|
97
|
+
receipt_path: Path,
|
|
98
|
+
primary_path: Path,
|
|
99
|
+
expected_operation: dict[str, Any] | None,
|
|
100
|
+
trust_root_path: Path,
|
|
101
|
+
expected_signer_fingerprint: str,
|
|
102
|
+
) -> bool:
|
|
103
|
+
receipt = load_json(stable_read_bytes(receipt_path))
|
|
104
|
+
primary = stable_read_bytes(primary_path)
|
|
105
|
+
if receipt.get("schemaVersion") != "agent-neutrality-detached-receipt.v3":
|
|
106
|
+
return False
|
|
107
|
+
if expected_operation is not None and receipt.get("operation") != expected_operation:
|
|
108
|
+
return False
|
|
109
|
+
primary_identity = receipt.get("primaryArtifact")
|
|
110
|
+
if not isinstance(primary_identity, dict):
|
|
111
|
+
return False
|
|
112
|
+
if primary_identity.get("sha256") != sha256_hex(primary):
|
|
113
|
+
return False
|
|
114
|
+
if primary_identity.get("bytes") != len(primary):
|
|
115
|
+
return False
|
|
116
|
+
trust_root = load_json(stable_read_bytes(trust_root_path))
|
|
117
|
+
public_key = _public_key_for_fingerprint(trust_root, expected_signer_fingerprint)
|
|
118
|
+
claims_digest = receipt.get("claimsDigest")
|
|
119
|
+
signature = receipt.get("signature")
|
|
120
|
+
if not isinstance(claims_digest, str) or not isinstance(signature, str):
|
|
121
|
+
return False
|
|
122
|
+
return verify(public_key, RECEIPT_DOMAIN + claims_digest.encode("ascii"), bytes.fromhex(signature))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _public_key_for_fingerprint(trust_root: dict[str, Any], expected_fingerprint: str) -> bytes:
|
|
126
|
+
if trust_root.get("schemaVersion") != "agent-neutrality-trust-root.v1":
|
|
127
|
+
raise NeutralityError("unsupported trust root schemaVersion")
|
|
128
|
+
for signer in trust_root.get("authorizedSigners", []):
|
|
129
|
+
if not isinstance(signer, dict) or signer.get("fingerprint") != expected_fingerprint:
|
|
130
|
+
continue
|
|
131
|
+
public_key = bytes.fromhex(str(signer.get("publicKeyHex")))
|
|
132
|
+
if fingerprint(public_key) != expected_fingerprint:
|
|
133
|
+
raise NeutralityError("trust root signer fingerprint mismatch")
|
|
134
|
+
return public_key
|
|
135
|
+
raise NeutralityError("expected signer is not authorized by trust root")
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Repository neutrality scanner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import stat
|
|
8
|
+
import subprocess
|
|
9
|
+
import zipfile
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Iterable, Pattern
|
|
13
|
+
|
|
14
|
+
from .canonical import canonical_bytes, sha256_hex
|
|
15
|
+
from .errors import NeutralityError
|
|
16
|
+
from .paths import stable_read_bytes, validate_output_paths, validate_repository_relative_path
|
|
17
|
+
from .policy import NeutralityPolicy
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class NeutralityFinding:
|
|
22
|
+
source: str
|
|
23
|
+
ruleId: str
|
|
24
|
+
category: str
|
|
25
|
+
|
|
26
|
+
def to_json(self) -> dict[str, str]:
|
|
27
|
+
return {"source": self.source, "ruleId": self.ruleId, "category": self.category}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class NeutralityReport:
|
|
32
|
+
scope: str
|
|
33
|
+
findings: list[NeutralityFinding] = field(default_factory=list)
|
|
34
|
+
skipped_inputs: int = 0
|
|
35
|
+
opaque_inputs: int = 0
|
|
36
|
+
read_races: int = 0
|
|
37
|
+
incomplete_scans: int = 0
|
|
38
|
+
unsupported_archives: int = 0
|
|
39
|
+
archive_limit_breaches: int = 0
|
|
40
|
+
occupied_output_conflicts: int = 0
|
|
41
|
+
path_alias_conflicts: int = 0
|
|
42
|
+
scanned_files: int = 0
|
|
43
|
+
scanned_git_objects: int = 0
|
|
44
|
+
working_tree_digest: str = "0" * 64
|
|
45
|
+
git_object_set_digest: str = "0" * 64
|
|
46
|
+
|
|
47
|
+
def to_json(self, operation: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
counters = {
|
|
49
|
+
"findings": len(self.findings),
|
|
50
|
+
"skippedInputs": self.skipped_inputs,
|
|
51
|
+
"opaqueInputs": self.opaque_inputs,
|
|
52
|
+
"readRaces": self.read_races,
|
|
53
|
+
"incompleteScans": self.incomplete_scans,
|
|
54
|
+
"unsupportedArchives": self.unsupported_archives,
|
|
55
|
+
"archiveLimitBreaches": self.archive_limit_breaches,
|
|
56
|
+
"occupiedOutputConflicts": self.occupied_output_conflicts,
|
|
57
|
+
"pathAliasConflicts": self.path_alias_conflicts,
|
|
58
|
+
}
|
|
59
|
+
subject = {
|
|
60
|
+
"scope": self.scope,
|
|
61
|
+
"counters": counters,
|
|
62
|
+
"workingTreeDigest": self.working_tree_digest,
|
|
63
|
+
"gitObjectSetDigest": self.git_object_set_digest,
|
|
64
|
+
"findingIds": [finding.ruleId for finding in self.findings],
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
"schemaVersion": "agent-neutrality-report.v1",
|
|
68
|
+
"operation": operation,
|
|
69
|
+
"scope": self.scope,
|
|
70
|
+
"counters": counters,
|
|
71
|
+
"scanned": {
|
|
72
|
+
"files": self.scanned_files,
|
|
73
|
+
"gitObjects": self.scanned_git_objects,
|
|
74
|
+
},
|
|
75
|
+
"digests": {
|
|
76
|
+
"workingTreeDigest": self.working_tree_digest,
|
|
77
|
+
"gitObjectSetDigest": self.git_object_set_digest,
|
|
78
|
+
"subjectDigest": sha256_hex(canonical_bytes(subject)),
|
|
79
|
+
},
|
|
80
|
+
"findings": [finding.to_json() for finding in self.findings],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class ArchiveScanBudget:
|
|
86
|
+
archives: int = 0
|
|
87
|
+
entries: int = 0
|
|
88
|
+
expanded_bytes: int = 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def scan_repository(
|
|
92
|
+
*,
|
|
93
|
+
workspace_root: Path,
|
|
94
|
+
policy: NeutralityPolicy,
|
|
95
|
+
deny_literals: Iterable[str],
|
|
96
|
+
deny_regexes: Iterable[str],
|
|
97
|
+
scope: str,
|
|
98
|
+
output_paths: list[Path],
|
|
99
|
+
) -> NeutralityReport:
|
|
100
|
+
report = NeutralityReport(scope=scope)
|
|
101
|
+
try:
|
|
102
|
+
normalized_outputs = validate_output_paths(output_paths)
|
|
103
|
+
except NeutralityError:
|
|
104
|
+
report.path_alias_conflicts += 1
|
|
105
|
+
normalized_outputs = []
|
|
106
|
+
for output_path in output_paths:
|
|
107
|
+
if (workspace_root / output_path).exists():
|
|
108
|
+
report.occupied_output_conflicts += 1
|
|
109
|
+
|
|
110
|
+
literal_rules = tuple(deny_literals) + policy.deny_literals
|
|
111
|
+
regex_rules = tuple(re.compile(value) for value in deny_regexes) + policy.deny_regexes
|
|
112
|
+
archive_budget = ArchiveScanBudget()
|
|
113
|
+
|
|
114
|
+
worktree_entries: list[dict[str, Any]] = []
|
|
115
|
+
for rel_path in _walk_repository_files(workspace_root, policy):
|
|
116
|
+
path = workspace_root / rel_path
|
|
117
|
+
try:
|
|
118
|
+
validate_repository_relative_path(rel_path)
|
|
119
|
+
data = stable_read_bytes(path, max_bytes=policy.max_file_bytes)
|
|
120
|
+
except NeutralityError:
|
|
121
|
+
report.read_races += 1
|
|
122
|
+
continue
|
|
123
|
+
report.scanned_files += 1
|
|
124
|
+
report.findings.extend(_match_data(rel_path, data, literal_rules, regex_rules))
|
|
125
|
+
_scan_archive(rel_path, data, policy, report, literal_rules, regex_rules, archive_budget, depth=1)
|
|
126
|
+
worktree_entries.append({"path": rel_path, "sha256": sha256_hex(data), "bytes": len(data)})
|
|
127
|
+
|
|
128
|
+
git_entries: list[dict[str, Any]] = []
|
|
129
|
+
if scope == "full-repository" and (workspace_root / ".git").exists():
|
|
130
|
+
for object_id, data in _git_objects(workspace_root, policy):
|
|
131
|
+
report.scanned_git_objects += 1
|
|
132
|
+
report.findings.extend(_match_data(f"git:{object_id}", data, literal_rules, regex_rules))
|
|
133
|
+
git_entries.append({"object": object_id, "sha256": sha256_hex(data), "bytes": len(data)})
|
|
134
|
+
|
|
135
|
+
report.working_tree_digest = sha256_hex(canonical_bytes({"files": worktree_entries, "outputs": normalized_outputs}))
|
|
136
|
+
report.git_object_set_digest = sha256_hex(canonical_bytes({"objects": git_entries}))
|
|
137
|
+
return report
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _walk_repository_files(workspace_root: Path, policy: NeutralityPolicy) -> Iterable[str]:
|
|
141
|
+
for root, dirnames, filenames in os.walk(workspace_root):
|
|
142
|
+
root_path = Path(root)
|
|
143
|
+
rel_root = root_path.relative_to(workspace_root).as_posix()
|
|
144
|
+
dirnames[:] = [
|
|
145
|
+
dirname
|
|
146
|
+
for dirname in dirnames
|
|
147
|
+
if not _excluded(_join_rel(rel_root, dirname), policy.path_excludes)
|
|
148
|
+
]
|
|
149
|
+
for filename in filenames:
|
|
150
|
+
rel_path = _join_rel(rel_root, filename)
|
|
151
|
+
if _excluded(rel_path, policy.path_excludes):
|
|
152
|
+
continue
|
|
153
|
+
mode = os.lstat(root_path / filename).st_mode
|
|
154
|
+
if stat.S_ISREG(mode):
|
|
155
|
+
yield rel_path
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _join_rel(root: str, name: str) -> str:
|
|
159
|
+
return name if root == "." else f"{root}/{name}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _excluded(path: str, excludes: tuple[Pattern[str], ...]) -> bool:
|
|
163
|
+
return any(pattern.search(path) for pattern in excludes)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _match_data(
|
|
167
|
+
source: str,
|
|
168
|
+
data: bytes,
|
|
169
|
+
literal_rules: tuple[str, ...],
|
|
170
|
+
regex_rules: tuple[Pattern[str], ...],
|
|
171
|
+
) -> list[NeutralityFinding]:
|
|
172
|
+
findings: list[NeutralityFinding] = []
|
|
173
|
+
text = data.decode("utf-8", errors="ignore")
|
|
174
|
+
for index, literal in enumerate(literal_rules):
|
|
175
|
+
if literal and literal in text:
|
|
176
|
+
findings.append(NeutralityFinding(source=source, ruleId=f"literal:{index}", category="deny-literal"))
|
|
177
|
+
for index, pattern in enumerate(regex_rules):
|
|
178
|
+
if pattern.search(text):
|
|
179
|
+
findings.append(NeutralityFinding(source=source, ruleId=f"regex:{index}", category="deny-regex"))
|
|
180
|
+
return findings
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _scan_archive(
|
|
184
|
+
source: str,
|
|
185
|
+
data: bytes,
|
|
186
|
+
policy: NeutralityPolicy,
|
|
187
|
+
report: NeutralityReport,
|
|
188
|
+
literal_rules: tuple[str, ...],
|
|
189
|
+
regex_rules: tuple[Pattern[str], ...],
|
|
190
|
+
archive_budget: ArchiveScanBudget,
|
|
191
|
+
*,
|
|
192
|
+
depth: int,
|
|
193
|
+
) -> None:
|
|
194
|
+
if not data.startswith(b"PK\x03\x04"):
|
|
195
|
+
return
|
|
196
|
+
import io
|
|
197
|
+
|
|
198
|
+
if depth > policy.max_archive_nesting_depth:
|
|
199
|
+
report.archive_limit_breaches += 1
|
|
200
|
+
return
|
|
201
|
+
archive_budget.archives += 1
|
|
202
|
+
if archive_budget.archives > policy.max_archives_per_subject:
|
|
203
|
+
report.archive_limit_breaches += 1
|
|
204
|
+
return
|
|
205
|
+
if len(data) > policy.max_compressed_bytes_per_archive:
|
|
206
|
+
report.archive_limit_breaches += 1
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
|
211
|
+
names: set[str] = set()
|
|
212
|
+
infos = archive.infolist()
|
|
213
|
+
if len(infos) > policy.max_archive_entries:
|
|
214
|
+
report.archive_limit_breaches += 1
|
|
215
|
+
return
|
|
216
|
+
expanded_bytes = sum(info.file_size for info in infos)
|
|
217
|
+
compressed_bytes = sum(info.compress_size for info in infos)
|
|
218
|
+
if expanded_bytes > policy.max_expanded_bytes_per_archive:
|
|
219
|
+
report.archive_limit_breaches += 1
|
|
220
|
+
return
|
|
221
|
+
if _ratio_exceeds(expanded_bytes, compressed_bytes, policy.max_compression_ratio):
|
|
222
|
+
report.archive_limit_breaches += 1
|
|
223
|
+
return
|
|
224
|
+
if archive_budget.entries + len(infos) > policy.max_entries_per_subject:
|
|
225
|
+
report.archive_limit_breaches += 1
|
|
226
|
+
return
|
|
227
|
+
if archive_budget.expanded_bytes + expanded_bytes > policy.max_expanded_bytes_per_subject:
|
|
228
|
+
report.archive_limit_breaches += 1
|
|
229
|
+
return
|
|
230
|
+
archive_budget.entries += len(infos)
|
|
231
|
+
archive_budget.expanded_bytes += expanded_bytes
|
|
232
|
+
for info in infos:
|
|
233
|
+
key = info.filename.casefold()
|
|
234
|
+
if key in names or info.filename.startswith("/") or ".." in Path(info.filename).parts:
|
|
235
|
+
report.archive_limit_breaches += 1
|
|
236
|
+
return
|
|
237
|
+
names.add(key)
|
|
238
|
+
if info.file_size > policy.max_expanded_entry_bytes:
|
|
239
|
+
report.archive_limit_breaches += 1
|
|
240
|
+
return
|
|
241
|
+
if _ratio_exceeds(info.file_size, info.compress_size, policy.max_compression_ratio):
|
|
242
|
+
report.archive_limit_breaches += 1
|
|
243
|
+
return
|
|
244
|
+
if info.is_dir():
|
|
245
|
+
continue
|
|
246
|
+
content = archive.read(info, pwd=None)
|
|
247
|
+
report.findings.extend(_match_data(f"{source}!{info.filename}", content, literal_rules, regex_rules))
|
|
248
|
+
_scan_archive(
|
|
249
|
+
f"{source}!{info.filename}",
|
|
250
|
+
content,
|
|
251
|
+
policy,
|
|
252
|
+
report,
|
|
253
|
+
literal_rules,
|
|
254
|
+
regex_rules,
|
|
255
|
+
archive_budget,
|
|
256
|
+
depth=depth + 1,
|
|
257
|
+
)
|
|
258
|
+
except (zipfile.BadZipFile, RuntimeError):
|
|
259
|
+
report.unsupported_archives += 1
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _ratio_exceeds(expanded_bytes: int, compressed_bytes: int, max_ratio: int) -> bool:
|
|
263
|
+
if expanded_bytes <= 0:
|
|
264
|
+
return False
|
|
265
|
+
if compressed_bytes <= 0:
|
|
266
|
+
return True
|
|
267
|
+
return expanded_bytes > compressed_bytes * max_ratio
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _git_objects(workspace_root: Path, policy: NeutralityPolicy) -> Iterable[tuple[str, bytes]]:
|
|
271
|
+
try:
|
|
272
|
+
objects = subprocess.run(
|
|
273
|
+
["git", "rev-list", "--objects", "--all", "--reflog"],
|
|
274
|
+
cwd=workspace_root,
|
|
275
|
+
check=True,
|
|
276
|
+
stdout=subprocess.PIPE,
|
|
277
|
+
stderr=subprocess.PIPE,
|
|
278
|
+
text=True,
|
|
279
|
+
).stdout.splitlines()
|
|
280
|
+
except (OSError, subprocess.CalledProcessError):
|
|
281
|
+
return
|
|
282
|
+
seen: set[str] = set()
|
|
283
|
+
for line in objects:
|
|
284
|
+
object_id = line.split(" ", 1)[0]
|
|
285
|
+
if object_id in seen:
|
|
286
|
+
continue
|
|
287
|
+
seen.add(object_id)
|
|
288
|
+
try:
|
|
289
|
+
size = int(
|
|
290
|
+
subprocess.run(
|
|
291
|
+
["git", "cat-file", "-s", object_id],
|
|
292
|
+
cwd=workspace_root,
|
|
293
|
+
check=True,
|
|
294
|
+
stdout=subprocess.PIPE,
|
|
295
|
+
stderr=subprocess.PIPE,
|
|
296
|
+
text=True,
|
|
297
|
+
).stdout.strip()
|
|
298
|
+
)
|
|
299
|
+
if size > policy.max_object_bytes:
|
|
300
|
+
continue
|
|
301
|
+
data = subprocess.run(
|
|
302
|
+
["git", "cat-file", "-p", object_id],
|
|
303
|
+
cwd=workspace_root,
|
|
304
|
+
check=True,
|
|
305
|
+
stdout=subprocess.PIPE,
|
|
306
|
+
stderr=subprocess.PIPE,
|
|
307
|
+
).stdout
|
|
308
|
+
except (OSError, subprocess.CalledProcessError, ValueError):
|
|
309
|
+
continue
|
|
310
|
+
yield object_id, data
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Stdlib unittest runner for controller validation commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
if __package__ in (None, ""):
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
14
|
+
|
|
15
|
+
from agent_lifecycle.neutrality.canonical import load_json, write_json_create
|
|
16
|
+
from agent_lifecycle.neutrality.errors import NeutralityError, write_neutrality_error
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv: list[str] | None = None) -> int:
|
|
20
|
+
parser = argparse.ArgumentParser(prog="agent-lifecycle-unittest-runner")
|
|
21
|
+
parser.add_argument("--operation-request", required=True)
|
|
22
|
+
parser.add_argument("--start-directory", required=True)
|
|
23
|
+
parser.add_argument("--pattern", default="test*.py")
|
|
24
|
+
parser.add_argument("--report", required=True)
|
|
25
|
+
args = parser.parse_args(argv)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
_require_protocol_output(
|
|
29
|
+
request_path=args.operation_request,
|
|
30
|
+
output_path=args.report,
|
|
31
|
+
schema_version="agent-lifecycle-unittest-report.v1",
|
|
32
|
+
)
|
|
33
|
+
result = _run_suite(args.start_directory, args.pattern)
|
|
34
|
+
report = _build_report(args.operation_request, args.start_directory, args.pattern, result)
|
|
35
|
+
write_json_create(Path(args.report), report)
|
|
36
|
+
return 0 if report["verdict"] == "PASS" else 1
|
|
37
|
+
except NeutralityError as exc:
|
|
38
|
+
write_neutrality_error(exc, sys.stdout)
|
|
39
|
+
return 2
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _run_suite(start_directory: str, pattern: str) -> unittest.TestResult:
|
|
43
|
+
loader = unittest.TestLoader()
|
|
44
|
+
suite = loader.discover(start_dir=start_directory, pattern=pattern)
|
|
45
|
+
runner = unittest.TextTestRunner(stream=sys.stderr, verbosity=1)
|
|
46
|
+
return runner.run(suite)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_report(
|
|
50
|
+
operation_request_path: str,
|
|
51
|
+
start_directory: str,
|
|
52
|
+
pattern: str,
|
|
53
|
+
result: unittest.TestResult,
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
request = load_json(Path(operation_request_path).read_bytes())
|
|
56
|
+
claims = request.get("claims")
|
|
57
|
+
if not isinstance(claims, dict):
|
|
58
|
+
raise NeutralityError("operation request claims are missing")
|
|
59
|
+
operation_keys = [
|
|
60
|
+
"runId",
|
|
61
|
+
"packageId",
|
|
62
|
+
"taskId",
|
|
63
|
+
"attempt",
|
|
64
|
+
"operationId",
|
|
65
|
+
"commandId",
|
|
66
|
+
"commandOperationId",
|
|
67
|
+
"planDigest",
|
|
68
|
+
"sourceRevision",
|
|
69
|
+
"projectionDigest",
|
|
70
|
+
]
|
|
71
|
+
return {
|
|
72
|
+
"schemaVersion": "agent-lifecycle-unittest-report.v1",
|
|
73
|
+
"operation": {
|
|
74
|
+
"schemaVersion": "agent-local-unittest-operation.v1",
|
|
75
|
+
"requestClaimsDigest": request.get("claimsDigest"),
|
|
76
|
+
**{key: claims.get(key) for key in operation_keys},
|
|
77
|
+
},
|
|
78
|
+
"suite": {
|
|
79
|
+
"startDirectory": start_directory,
|
|
80
|
+
"pattern": pattern,
|
|
81
|
+
"testsRun": result.testsRun,
|
|
82
|
+
"failures": len(result.failures),
|
|
83
|
+
"errors": len(result.errors),
|
|
84
|
+
"skipped": len(result.skipped),
|
|
85
|
+
"expectedFailures": len(result.expectedFailures),
|
|
86
|
+
"unexpectedSuccesses": len(result.unexpectedSuccesses),
|
|
87
|
+
},
|
|
88
|
+
"verdict": "PASS" if result.wasSuccessful() else "FAIL",
|
|
89
|
+
"generatedAt": datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _require_protocol_output(
|
|
94
|
+
*,
|
|
95
|
+
request_path: str,
|
|
96
|
+
output_path: str,
|
|
97
|
+
schema_version: str,
|
|
98
|
+
) -> None:
|
|
99
|
+
request = load_json(Path(request_path).read_bytes())
|
|
100
|
+
claims = request.get("claims")
|
|
101
|
+
outputs = claims.get("protocolOutputs") if isinstance(claims, dict) else None
|
|
102
|
+
if not isinstance(outputs, list):
|
|
103
|
+
raise NeutralityError("operation request protocolOutputs are missing")
|
|
104
|
+
for item in outputs:
|
|
105
|
+
if (
|
|
106
|
+
isinstance(item, dict)
|
|
107
|
+
and item.get("path") == output_path
|
|
108
|
+
and item.get("schemaVersion") == schema_version
|
|
109
|
+
and item.get("createNoReplace") is True
|
|
110
|
+
):
|
|
111
|
+
return
|
|
112
|
+
raise NeutralityError("report path is not authorized by operation request")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Agent-ready plan validation primitives."""
|
|
2
|
+
|
|
3
|
+
from agent_lifecycle.planning.acceptance_markdown import validate_acceptance_checklist
|
|
4
|
+
from agent_lifecycle.planning.continuity import (
|
|
5
|
+
build_plan_snapshot,
|
|
6
|
+
reconcile_plan_snapshot,
|
|
7
|
+
render_plan_handoff,
|
|
8
|
+
require_reconciliation_pass,
|
|
9
|
+
require_repository_references_pass,
|
|
10
|
+
validate_repository_references,
|
|
11
|
+
)
|
|
12
|
+
from agent_lifecycle.planning.completeness import (
|
|
13
|
+
build_plan_completeness_profile,
|
|
14
|
+
load_plan_completeness_profile,
|
|
15
|
+
require_plan_completeness_pass,
|
|
16
|
+
validate_plan_completeness,
|
|
17
|
+
validate_plan_completeness_profile,
|
|
18
|
+
)
|
|
19
|
+
from agent_lifecycle.planning.tiering import resolve_sdd_tier
|
|
20
|
+
from agent_lifecycle.planning.templates import (
|
|
21
|
+
build_task_template_library,
|
|
22
|
+
render_task_template,
|
|
23
|
+
require_task_template_validation_pass,
|
|
24
|
+
validate_task_template_library,
|
|
25
|
+
)
|
|
26
|
+
from agent_lifecycle.planning.validation import validate_plan_manifest
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"build_plan_snapshot",
|
|
30
|
+
"build_plan_completeness_profile",
|
|
31
|
+
"build_task_template_library",
|
|
32
|
+
"load_plan_completeness_profile",
|
|
33
|
+
"reconcile_plan_snapshot",
|
|
34
|
+
"render_task_template",
|
|
35
|
+
"render_plan_handoff",
|
|
36
|
+
"require_reconciliation_pass",
|
|
37
|
+
"require_repository_references_pass",
|
|
38
|
+
"require_plan_completeness_pass",
|
|
39
|
+
"require_task_template_validation_pass",
|
|
40
|
+
"resolve_sdd_tier",
|
|
41
|
+
"validate_acceptance_checklist",
|
|
42
|
+
"validate_plan_completeness",
|
|
43
|
+
"validate_plan_completeness_profile",
|
|
44
|
+
"validate_plan_manifest",
|
|
45
|
+
"validate_repository_references",
|
|
46
|
+
"validate_task_template_library",
|
|
47
|
+
]
|