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,124 @@
1
+ """Small pure-Python Ed25519 implementation for local portability.
2
+
3
+ The code follows the formulas from RFC 8032 and intentionally exposes only the
4
+ operations required by the lifecycle receipts.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+
11
+ P = 2**255 - 19
12
+ Q = 2**252 + 27742317777372353535851937790883648493
13
+ D = -121665 * pow(121666, P - 2, P) % P
14
+ I = pow(2, (P - 1) // 4, P)
15
+
16
+
17
+ def _x_recover(y: int) -> int:
18
+ xx = (y * y - 1) * pow(D * y * y + 1, P - 2, P)
19
+ x = pow(xx, (P + 3) // 8, P)
20
+ if (x * x - xx) % P != 0:
21
+ x = (x * I) % P
22
+ if x % 2 != 0:
23
+ x = P - x
24
+ return x
25
+
26
+
27
+ B = (_x_recover(4 * pow(5, P - 2, P) % P), 4 * pow(5, P - 2, P) % P)
28
+
29
+
30
+ def _is_on_curve(point: tuple[int, int]) -> bool:
31
+ x, y = point
32
+ return (-x * x + y * y - 1 - D * x * x * y * y) % P == 0
33
+
34
+
35
+ def _point_add(left: tuple[int, int], right: tuple[int, int]) -> tuple[int, int]:
36
+ x1, y1 = left
37
+ x2, y2 = right
38
+ denominator = pow(1 + D * x1 * x2 * y1 * y2, P - 2, P)
39
+ x3 = (x1 * y2 + x2 * y1) * denominator % P
40
+ denominator = pow(1 - D * x1 * x2 * y1 * y2, P - 2, P)
41
+ y3 = (y1 * y2 + x1 * x2) * denominator % P
42
+ return x3, y3
43
+
44
+
45
+ def _scalar_mult(point: tuple[int, int], scalar: int) -> tuple[int, int]:
46
+ if scalar == 0:
47
+ return 0, 1
48
+ half = _scalar_mult(point, scalar // 2)
49
+ doubled = _point_add(half, half)
50
+ if scalar & 1:
51
+ return _point_add(doubled, point)
52
+ return doubled
53
+
54
+
55
+ def _encode_int(value: int) -> bytes:
56
+ return value.to_bytes(32, "little")
57
+
58
+
59
+ def _encode_point(point: tuple[int, int]) -> bytes:
60
+ x, y = point
61
+ encoded = bytearray(_encode_int(y))
62
+ encoded[31] |= (x & 1) << 7
63
+ return bytes(encoded)
64
+
65
+
66
+ def _decode_point(data: bytes) -> tuple[int, int]:
67
+ if len(data) != 32:
68
+ raise ValueError("encoded point must be 32 bytes")
69
+ y = int.from_bytes(data, "little") & ((1 << 255) - 1)
70
+ x = _x_recover(y)
71
+ if (x & 1) != (data[31] >> 7):
72
+ x = P - x
73
+ point = (x, y)
74
+ if not _is_on_curve(point):
75
+ raise ValueError("decoded point is not on curve")
76
+ return point
77
+
78
+
79
+ def _hint(data: bytes) -> int:
80
+ return int.from_bytes(hashlib.sha512(data).digest(), "little")
81
+
82
+
83
+ def _secret_scalar(seed: bytes) -> tuple[int, bytes]:
84
+ if len(seed) != 32:
85
+ raise ValueError("Ed25519 seed must be 32 bytes")
86
+ digest = hashlib.sha512(seed).digest()
87
+ scalar = int.from_bytes(digest[:32], "little")
88
+ scalar &= (1 << 254) - 8
89
+ scalar |= 1 << 254
90
+ return scalar, digest[32:]
91
+
92
+
93
+ def publickey_from_seed(seed: bytes) -> bytes:
94
+ scalar, _prefix = _secret_scalar(seed)
95
+ return _encode_point(_scalar_mult(B, scalar))
96
+
97
+
98
+ def sign(seed: bytes, message: bytes) -> bytes:
99
+ scalar, prefix = _secret_scalar(seed)
100
+ public_key = publickey_from_seed(seed)
101
+ r = _hint(prefix + message) % Q
102
+ encoded_r = _encode_point(_scalar_mult(B, r))
103
+ h = _hint(encoded_r + public_key + message) % Q
104
+ s = (r + h * scalar) % Q
105
+ return encoded_r + _encode_int(s)
106
+
107
+
108
+ def verify(public_key: bytes, message: bytes, signature: bytes) -> bool:
109
+ if len(public_key) != 32 or len(signature) != 64:
110
+ return False
111
+ try:
112
+ point_a = _decode_point(public_key)
113
+ point_r = _decode_point(signature[:32])
114
+ except ValueError:
115
+ return False
116
+ s = int.from_bytes(signature[32:], "little")
117
+ if s >= Q:
118
+ return False
119
+ h = _hint(signature[:32] + public_key + message) % Q
120
+ return _scalar_mult(B, s) == _point_add(point_r, _scalar_mult(point_a, h))
121
+
122
+
123
+ def fingerprint(public_key: bytes) -> str:
124
+ return "ed25519:" + hashlib.sha256(public_key).hexdigest()
@@ -0,0 +1,89 @@
1
+ """Neutrality-specific exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, TextIO
6
+
7
+ from agent_lifecycle.contracts.canonical import canonical_bytes
8
+ from agent_lifecycle.contracts.errors import LifecycleError
9
+
10
+ _CODE_RULES: tuple[tuple[str, str], ...] = (
11
+ ("signing key is required", "neutrality-signing-key-required"),
12
+ ("signing key fingerprint does not match", "neutrality-signing-key-mismatch"),
13
+ ("signing key public key is not authorized", "neutrality-signing-key-unauthorized"),
14
+ ("missing signature", "neutrality-authority-missing-signature"),
15
+ ("unsupported signature algorithm", "neutrality-unsupported-signature-algorithm"),
16
+ ("signer fingerprint mismatch", "neutrality-authority-signer-mismatch"),
17
+ ("gate receipt signature verification failed", "neutrality-gate-signature-invalid"),
18
+ ("deny authority signature verification failed", "neutrality-authority-signature-invalid"),
19
+ ("signature verification failed", "neutrality-signature-invalid"),
20
+ ("authorizedsigners must be a list", "neutrality-trust-root-invalid"),
21
+ ("trust root signer public key does not match", "neutrality-trust-root-signer-mismatch"),
22
+ ("expected signer is not authorized", "neutrality-signer-unauthorized"),
23
+ ("unsupported trust root schemaversion", "neutrality-trust-root-schema-unsupported"),
24
+ ("unsupported neutrality policy schemaversion", "neutrality-policy-schema-unsupported"),
25
+ ("expected agent-neutrality", "neutrality-authority-schema-mismatch"),
26
+ ("expiresat must be after issuedat", "neutrality-authority-invalid-window"),
27
+ ("validity window is too wide", "neutrality-authority-invalid-window"),
28
+ ("issuedat is in the future", "neutrality-authority-not-yet-valid"),
29
+ ("authority is stale", "neutrality-authority-stale"),
30
+ ("timestamps must include timezone", "neutrality-timestamp-invalid"),
31
+ ("must be a non-empty string", "neutrality-field-invalid"),
32
+ ("must be an object", "neutrality-field-invalid"),
33
+ ("must be a list of strings", "neutrality-field-invalid"),
34
+ ("must be a positive integer", "neutrality-field-invalid"),
35
+ ("external authority paths must be absolute", "neutrality-external-authority-path-invalid"),
36
+ ("external authority path is inside a forbidden root", "neutrality-external-authority-forbidden-root"),
37
+ ("external authority path must not be a symlink", "neutrality-external-authority-symlink"),
38
+ ("external authority path must be a regular file", "neutrality-external-authority-not-regular"),
39
+ ("external authority path must not have multiple hard links", "neutrality-external-authority-hard-link"),
40
+ ("not a regular file", "neutrality-input-not-regular"),
41
+ ("file exceeds max bytes", "neutrality-input-too-large"),
42
+ ("read exceeds max bytes", "neutrality-input-too-large"),
43
+ ("stable read identity changed", "neutrality-stable-read-race"),
44
+ ("invalid repository-relative path", "neutrality-repository-path-invalid"),
45
+ ("repository-relative path is too long", "neutrality-repository-path-invalid"),
46
+ ("output path alias conflict", "neutrality-output-alias-conflict"),
47
+ ("operation request claims are missing", "neutrality-operation-request-invalid"),
48
+ ("operation request protocoloutputs are missing", "neutrality-operation-request-invalid"),
49
+ ("report path is not authorized", "neutrality-output-not-authorized"),
50
+ ("requested report path is not authorized", "neutrality-output-not-authorized"),
51
+ ("existing output paths do not match", "neutrality-output-occupied"),
52
+ ("operation output path is occupied", "neutrality-output-occupied"),
53
+ ("neutrality findings are non-zero", "neutrality-findings-nonzero"),
54
+ ("neutrality scan counters are non-zero", "neutrality-counters-nonzero"),
55
+ ("neutrality gate counters are non-zero", "neutrality-counters-nonzero"),
56
+ ("receipt verification failed", "neutrality-receipt-verification-failed"),
57
+ ("gate receipt path is already occupied", "neutrality-gate-receipt-occupied"),
58
+ ("gate receipt generatedat is missing", "neutrality-gate-generated-at-missing"),
59
+ ("gate receipt attestation is missing", "neutrality-gate-attestation-missing"),
60
+ ("gate receipt attestation binding mismatch", "neutrality-gate-attestation-mismatch"),
61
+ ("gate receipt", "neutrality-gate-binding-mismatch"),
62
+ ("operation output must match receipt", "neutrality-gate-output-mismatch"),
63
+ ("gate receipt must be repository-relative", "neutrality-gate-receipt-path-invalid"),
64
+ ("gate receipt escapes workspace", "neutrality-gate-receipt-path-invalid"),
65
+ ("operation outputs must be repository-relative", "neutrality-output-path-invalid"),
66
+ ("operation output escapes workspace", "neutrality-output-path-invalid"),
67
+ ("required environment variable is not set", "neutrality-env-missing"),
68
+ ("invalid json", "neutrality-invalid-json"),
69
+ ("expected object", "neutrality-json-object-required"),
70
+ )
71
+
72
+
73
+ class NeutralityError(LifecycleError):
74
+ """A fail-closed neutrality contract violation."""
75
+
76
+ def __init__(self, message: str, *, code: str | None = None, details: dict[str, Any] | None = None):
77
+ super().__init__(code or neutrality_error_code(message), message, details or {})
78
+
79
+
80
+ def neutrality_error_code(message: str) -> str:
81
+ normalized = message.casefold()
82
+ for marker, code in _CODE_RULES:
83
+ if marker in normalized:
84
+ return code
85
+ return "neutrality-contract-violation"
86
+
87
+
88
+ def write_neutrality_error(exc: NeutralityError, stream: TextIO) -> None:
89
+ stream.write(canonical_bytes(exc.to_json()).decode("utf-8") + "\n")
@@ -0,0 +1,269 @@
1
+ """Controller-gate compatibility wrapper for neutrality scans."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import base64
7
+ import os
8
+ import re
9
+ import sys
10
+ from dataclasses import replace
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ if __package__ in (None, ""):
16
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
17
+
18
+ from agent_lifecycle.neutrality.authority import RECEIPT_DOMAIN, load_authority_bundle
19
+ from agent_lifecycle.neutrality.canonical import canonical_bytes, load_json, sha256_hex, write_json_create
20
+ from agent_lifecycle.neutrality.ed25519 import verify as verify_signature
21
+ from agent_lifecycle.neutrality.errors import NeutralityError, write_neutrality_error
22
+ from agent_lifecycle.neutrality.paths import resolve_repository_relative_root
23
+ from agent_lifecycle.neutrality.policy import load_policy
24
+ from agent_lifecycle.neutrality.scanner import scan_repository
25
+
26
+ POLICY_PATH = "policy/neutrality.policy.json"
27
+
28
+
29
+ def main(argv: list[str] | None = None) -> int:
30
+ parser = _parser()
31
+ args = parser.parse_args(argv)
32
+ try:
33
+ if args.command == "produce":
34
+ return _produce(args)
35
+ if args.command == "verify":
36
+ return _verify(args)
37
+ except NeutralityError as exc:
38
+ write_neutrality_error(exc, sys.stdout)
39
+ return 2
40
+ return 2
41
+
42
+
43
+ def _parser() -> argparse.ArgumentParser:
44
+ parser = argparse.ArgumentParser(prog="agent-lifecycle-neutrality-gate")
45
+ subparsers = parser.add_subparsers(dest="command", required=True)
46
+ for command in ("produce", "verify"):
47
+ gate = subparsers.add_parser(command)
48
+ gate.add_argument("--profile", required=True)
49
+ gate.add_argument("--scope", required=True, choices=["current-tree-complete", "full-repository"])
50
+ gate.add_argument("--gate-id", required=True)
51
+ gate.add_argument("--run-id", required=True)
52
+ gate.add_argument("--package-id", required=True)
53
+ gate.add_argument("--task-id", required=True)
54
+ gate.add_argument("--attempt", required=True, type=int)
55
+ gate.add_argument("--phase", required=True)
56
+ gate.add_argument("--operation-id", required=True)
57
+ gate.add_argument("--plan-digest", required=True)
58
+ gate.add_argument("--source-revision", required=True)
59
+ gate.add_argument("--workspace-root", required=True)
60
+ gate.add_argument("--artifact-root", default="plans/standalone-v1")
61
+ gate.add_argument("--operation-output", required=True)
62
+ gate.add_argument("--receipt", required=True)
63
+ return parser
64
+
65
+
66
+ def _produce(args: argparse.Namespace) -> int:
67
+ workspace_root = Path(args.workspace_root).resolve()
68
+ receipt_path = _receipt_path(workspace_root, args)
69
+ if receipt_path.exists():
70
+ raise NeutralityError("gate receipt path is already occupied")
71
+ authority = _authority(workspace_root, args, signing=True)
72
+ report = _scan(workspace_root, args, authority)
73
+ _require_zero(report)
74
+ generated_at = datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
75
+ claims = _claims(args, report, authority.authority_digest, generated_at)
76
+ claims_digest = sha256_hex(canonical_bytes(claims))
77
+ receipt = {
78
+ "schemaVersion": "agent-controller-gate-receipt.v1",
79
+ "gateId": args.gate_id,
80
+ "runId": args.run_id,
81
+ "packageId": args.package_id,
82
+ "taskId": args.task_id,
83
+ "attempt": args.attempt,
84
+ "phase": args.phase,
85
+ "operationId": args.operation_id,
86
+ "planDigest": args.plan_digest,
87
+ "sourceRevision": args.source_revision,
88
+ "verdict": "PASS",
89
+ "generatedAt": generated_at,
90
+ "attestation": {
91
+ "schemaVersion": "agent-controller-gate-attestation.v1",
92
+ "algorithm": "Ed25519",
93
+ "claimsDigest": claims_digest,
94
+ "subjectDigest": report["digests"]["subjectDigest"],
95
+ "scopeDigest": claims["scopeDigest"],
96
+ "authorityDigest": authority.authority_digest,
97
+ "signerFingerprint": _digest_fingerprint(authority.signer_fingerprint),
98
+ "signature": base64.b64encode(
99
+ bytes.fromhex(authority.sign_claims_digest(claims_digest))
100
+ ).decode("ascii"),
101
+ },
102
+ }
103
+ write_json_create(receipt_path, receipt)
104
+ print(f"neutrality-gate: wrote {receipt_path.relative_to(workspace_root)}")
105
+ return 0
106
+
107
+
108
+ def _verify(args: argparse.Namespace) -> int:
109
+ workspace_root = Path(args.workspace_root).resolve()
110
+ receipt_path = _receipt_path(workspace_root, args)
111
+ receipt = load_json(receipt_path.read_bytes())
112
+ expected = {
113
+ "schemaVersion": "agent-controller-gate-receipt.v1",
114
+ "gateId": args.gate_id,
115
+ "runId": args.run_id,
116
+ "packageId": args.package_id,
117
+ "taskId": args.task_id,
118
+ "attempt": args.attempt,
119
+ "phase": args.phase,
120
+ "operationId": args.operation_id,
121
+ "planDigest": args.plan_digest,
122
+ "sourceRevision": args.source_revision,
123
+ "verdict": "PASS",
124
+ }
125
+ for key, value in expected.items():
126
+ if receipt.get(key) != value:
127
+ raise NeutralityError(f"gate receipt {key} binding mismatch")
128
+ generated_at = receipt.get("generatedAt")
129
+ if not isinstance(generated_at, str) or not generated_at:
130
+ raise NeutralityError("gate receipt generatedAt is missing")
131
+ authority = _authority(workspace_root, args, signing=False)
132
+ report = _scan(workspace_root, args, authority)
133
+ _require_zero(report)
134
+ claims = _claims(args, report, authority.authority_digest, generated_at)
135
+ claims_digest = sha256_hex(canonical_bytes(claims))
136
+ attestation = receipt.get("attestation")
137
+ if not isinstance(attestation, dict):
138
+ raise NeutralityError("gate receipt attestation is missing")
139
+ if (
140
+ attestation.get("claimsDigest") != claims_digest
141
+ or attestation.get("subjectDigest") != report["digests"]["subjectDigest"]
142
+ or attestation.get("scopeDigest") != claims["scopeDigest"]
143
+ or attestation.get("authorityDigest") != authority.authority_digest
144
+ or attestation.get("signerFingerprint") != _digest_fingerprint(authority.signer_fingerprint)
145
+ ):
146
+ raise NeutralityError("gate receipt attestation binding mismatch")
147
+ signature = base64.b64decode(str(attestation.get("signature")), validate=True)
148
+ if not verify_signature(
149
+ bytes.fromhex(authority.signer_public_key_hex),
150
+ RECEIPT_DOMAIN + claims_digest.encode("ascii"),
151
+ signature,
152
+ ):
153
+ raise NeutralityError("gate receipt signature verification failed")
154
+ print("neutrality-gate: receipt verified")
155
+ return 0
156
+
157
+
158
+ def _authority(workspace_root: Path, args: argparse.Namespace, *, signing: bool):
159
+ return load_authority_bundle(
160
+ deny_authority_path=Path(_env("AGENT_LIFECYCLE_NEUTRALITY_DENY_AUTHORITY")),
161
+ trust_root_path=Path(_env("AGENT_LIFECYCLE_NEUTRALITY_TRUST_ROOT")),
162
+ signing_key_path=(
163
+ Path(_env("AGENT_LIFECYCLE_NEUTRALITY_SIGNING_KEY")) if signing else None
164
+ ),
165
+ expected_signer_fingerprint=_env("AGENT_LIFECYCLE_NEUTRALITY_SIGNER_FINGERPRINT"),
166
+ workspace_root=workspace_root,
167
+ artifact_root=_artifact_root(workspace_root, args.artifact_root),
168
+ )
169
+
170
+
171
+ def _scan(workspace_root: Path, args: argparse.Namespace, authority) -> dict[str, Any]:
172
+ policy = load_policy(workspace_root / POLICY_PATH)
173
+ receipt_path = _receipt_path(workspace_root, args).relative_to(workspace_root).as_posix()
174
+ policy = replace(
175
+ policy,
176
+ path_excludes=policy.path_excludes + (re.compile(f"^{re.escape(receipt_path)}$"),),
177
+ )
178
+ profile = load_json((workspace_root / args.profile).read_bytes())
179
+ report = scan_repository(
180
+ workspace_root=workspace_root,
181
+ policy=policy,
182
+ deny_literals=authority.deny_literals,
183
+ deny_regexes=authority.deny_regexes,
184
+ scope=args.scope,
185
+ output_paths=[],
186
+ ).to_json(
187
+ {
188
+ "schemaVersion": "agent-neutrality-gate-operation.v1",
189
+ "gateId": args.gate_id,
190
+ "runId": args.run_id,
191
+ "packageId": args.package_id,
192
+ "taskId": args.task_id,
193
+ "attempt": args.attempt,
194
+ "phase": args.phase,
195
+ "operationId": args.operation_id,
196
+ "planDigest": args.plan_digest,
197
+ "sourceRevision": args.source_revision,
198
+ "profileDigest": sha256_hex(canonical_bytes(profile)),
199
+ }
200
+ )
201
+ return report
202
+
203
+
204
+ def _claims(
205
+ args: argparse.Namespace,
206
+ report: dict[str, Any],
207
+ authority_digest: str,
208
+ generated_at: str,
209
+ ) -> dict[str, Any]:
210
+ return {
211
+ "schemaVersion": "agent-neutrality-controller-gate-claims.v1",
212
+ "gateId": args.gate_id,
213
+ "runId": args.run_id,
214
+ "packageId": args.package_id,
215
+ "taskId": args.task_id,
216
+ "attempt": args.attempt,
217
+ "phase": args.phase,
218
+ "operationId": args.operation_id,
219
+ "planDigest": args.plan_digest,
220
+ "sourceRevision": args.source_revision,
221
+ "scope": report["scope"],
222
+ "scopeDigest": sha256_hex(canonical_bytes({"scope": report["scope"]})),
223
+ "subjectDigest": report["digests"]["subjectDigest"],
224
+ "workingTreeDigest": report["digests"]["workingTreeDigest"],
225
+ "gitObjectSetDigest": report["digests"]["gitObjectSetDigest"],
226
+ "zeroCounters": report["counters"],
227
+ "authorityDigest": authority_digest,
228
+ "receiptPath": args.receipt,
229
+ "generatedAt": generated_at,
230
+ "verdict": "PASS",
231
+ }
232
+
233
+
234
+ def _require_zero(report: dict[str, Any]) -> None:
235
+ if any(int(value) != 0 for value in report["counters"].values()):
236
+ raise NeutralityError("neutrality gate counters are non-zero")
237
+
238
+
239
+ def _receipt_path(workspace_root: Path, args: argparse.Namespace) -> Path:
240
+ if args.operation_output != args.receipt:
241
+ raise NeutralityError("operation output must match receipt for controller gate v1")
242
+ path = Path(args.receipt)
243
+ if path.is_absolute():
244
+ raise NeutralityError("gate receipt must be repository-relative")
245
+ resolved = (workspace_root / path).resolve()
246
+ if not resolved.is_relative_to(workspace_root):
247
+ raise NeutralityError("gate receipt escapes workspace")
248
+ return resolved
249
+
250
+
251
+ def _artifact_root(workspace_root: Path, value: str) -> Path:
252
+ return resolve_repository_relative_root(workspace_root, value, label="artifact root")
253
+
254
+
255
+ def _digest_fingerprint(value: str) -> str:
256
+ if value.startswith("ed25519:"):
257
+ return value.split(":", 1)[1]
258
+ return value
259
+
260
+
261
+ def _env(name: str) -> str:
262
+ value = os.environ.get(name)
263
+ if not value:
264
+ raise NeutralityError(f"required environment variable is not set: {name}")
265
+ return value
266
+
267
+
268
+ if __name__ == "__main__":
269
+ raise SystemExit(main())
@@ -0,0 +1,92 @@
1
+ """Portable path and stable-read checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import stat
7
+ from pathlib import Path
8
+
9
+ from .errors import NeutralityError
10
+
11
+
12
+ def ensure_external_regular_file(
13
+ path: Path,
14
+ *,
15
+ workspace_root: Path,
16
+ artifact_root: Path,
17
+ release_root: Path | None = None,
18
+ ) -> Path:
19
+ if not path.is_absolute():
20
+ raise NeutralityError("external authority paths must be absolute")
21
+ resolved = path.resolve(strict=True)
22
+ roots = [workspace_root.resolve(), artifact_root.resolve()]
23
+ git_root = workspace_root / ".git"
24
+ if git_root.exists():
25
+ roots.append(git_root.resolve())
26
+ if release_root is not None:
27
+ roots.append(release_root.resolve())
28
+ for root in roots:
29
+ if resolved == root or resolved.is_relative_to(root):
30
+ raise NeutralityError("external authority path is inside a forbidden root")
31
+ first = os.lstat(resolved)
32
+ if stat.S_ISLNK(first.st_mode):
33
+ raise NeutralityError("external authority path must not be a symlink")
34
+ if not stat.S_ISREG(first.st_mode):
35
+ raise NeutralityError("external authority path must be a regular file")
36
+ if first.st_nlink > 1:
37
+ raise NeutralityError("external authority path must not have multiple hard links")
38
+ return resolved
39
+
40
+
41
+ def stable_read_bytes(path: Path, *, max_bytes: int | None = None) -> bytes:
42
+ before = os.stat(path, follow_symlinks=False)
43
+ if not stat.S_ISREG(before.st_mode):
44
+ raise NeutralityError(f"not a regular file: {path}")
45
+ if max_bytes is not None and before.st_size > max_bytes:
46
+ raise NeutralityError(f"file exceeds max bytes: {path}")
47
+ with path.open("rb") as handle:
48
+ data = handle.read()
49
+ after = os.stat(path, follow_symlinks=False)
50
+ identity_before = (before.st_dev, before.st_ino, before.st_mode, before.st_size, before.st_mtime_ns)
51
+ identity_after = (after.st_dev, after.st_ino, after.st_mode, after.st_size, after.st_mtime_ns)
52
+ if identity_before != identity_after:
53
+ raise NeutralityError(f"stable read identity changed: {path}")
54
+ if max_bytes is not None and len(data) > max_bytes:
55
+ raise NeutralityError(f"read exceeds max bytes: {path}")
56
+ return data
57
+
58
+
59
+ def validate_repository_relative_path(path: str) -> None:
60
+ if not path or path.startswith("/") or "\\" in path or "\x00" in path:
61
+ raise NeutralityError(f"invalid repository-relative path: {path}")
62
+ parts = path.split("/")
63
+ if any(part in ("", ".", "..") for part in parts):
64
+ raise NeutralityError(f"invalid repository-relative path: {path}")
65
+ if any(part.endswith(".") or part.endswith(" ") for part in parts):
66
+ raise NeutralityError(f"invalid repository-relative path: {path}")
67
+ if len(path.encode("utf-8")) > 4096:
68
+ raise NeutralityError(f"repository-relative path is too long: {path}")
69
+
70
+
71
+ def validate_output_paths(output_paths: list[Path]) -> list[str]:
72
+ seen: dict[str, str] = {}
73
+ normalized: list[str] = []
74
+ for path in output_paths:
75
+ path_text = path.as_posix()
76
+ validate_repository_relative_path(path_text)
77
+ key = path_text.casefold()
78
+ if key in seen:
79
+ raise NeutralityError(f"output path alias conflict: {seen[key]} vs {path_text}")
80
+ seen[key] = path_text
81
+ normalized.append(path_text)
82
+ return normalized
83
+
84
+
85
+ def resolve_repository_relative_root(workspace_root: Path, value: str, *, label: str) -> Path:
86
+ path = Path(value)
87
+ if path.is_absolute():
88
+ raise NeutralityError(f"{label} must be repository-relative")
89
+ resolved = (workspace_root / path).resolve()
90
+ if not resolved.is_relative_to(workspace_root):
91
+ raise NeutralityError(f"{label} escapes workspace")
92
+ return resolved
@@ -0,0 +1,76 @@
1
+ """Neutrality policy loading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Pattern
9
+
10
+ from .canonical import load_json
11
+ from .errors import NeutralityError
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class NeutralityPolicy:
16
+ raw: dict[str, Any]
17
+ path_excludes: tuple[Pattern[str], ...]
18
+ deny_literals: tuple[str, ...]
19
+ deny_regexes: tuple[Pattern[str], ...]
20
+ max_file_bytes: int
21
+ max_object_bytes: int
22
+ max_archive_nesting_depth: int
23
+ max_archives_per_subject: int
24
+ max_archive_entries: int
25
+ max_entries_per_subject: int
26
+ max_compressed_bytes_per_archive: int
27
+ max_expanded_bytes_per_archive: int
28
+ max_expanded_entry_bytes: int
29
+ max_expanded_bytes_per_subject: int
30
+ max_compression_ratio: int
31
+
32
+
33
+ def load_policy(path: Path) -> NeutralityPolicy:
34
+ document = load_json(path.read_bytes())
35
+ if document.get("schemaVersion") != "agent-lifecycle-neutrality-policy.v1":
36
+ raise NeutralityError("unsupported neutrality policy schemaVersion")
37
+ scan = _object(document, "scan")
38
+ archives = _object(document, "archives")
39
+ return NeutralityPolicy(
40
+ raw=document,
41
+ path_excludes=tuple(re.compile(value) for value in _string_list(document, "pathExcludes")),
42
+ deny_literals=tuple(_string_list(document, "denyLiterals")),
43
+ deny_regexes=tuple(re.compile(value) for value in _string_list(document, "denyRegexes")),
44
+ max_file_bytes=_positive_int(scan, "maxFileBytes", 16_777_216),
45
+ max_object_bytes=_positive_int(scan, "maxObjectBytes", 16_777_216),
46
+ max_archive_nesting_depth=_positive_int(archives, "maxArchiveNestingDepth", 4),
47
+ max_archives_per_subject=_positive_int(archives, "maxArchivesPerSubject", 1_000),
48
+ max_archive_entries=_positive_int(archives, "maxEntriesPerArchive", 10_000),
49
+ max_entries_per_subject=_positive_int(archives, "maxEntriesPerSubject", 100_000),
50
+ max_compressed_bytes_per_archive=_positive_int(archives, "maxCompressedBytesPerArchive", 536_870_912),
51
+ max_expanded_bytes_per_archive=_positive_int(archives, "maxExpandedBytesPerArchive", 2_147_483_648),
52
+ max_expanded_entry_bytes=_positive_int(archives, "maxExpandedBytesPerEntry", 268_435_456),
53
+ max_expanded_bytes_per_subject=_positive_int(archives, "maxExpandedBytesPerSubject", 4_294_967_296),
54
+ max_compression_ratio=_positive_int(archives, "maxCompressionRatio", 100),
55
+ )
56
+
57
+
58
+ def _string_list(document: dict[str, Any], key: str) -> list[str]:
59
+ value = document.get(key, [])
60
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
61
+ raise NeutralityError(f"{key} must be a list of strings")
62
+ return value
63
+
64
+
65
+ def _object(document: dict[str, Any], key: str) -> dict[str, Any]:
66
+ value = document.get(key, {})
67
+ if not isinstance(value, dict):
68
+ raise NeutralityError(f"{key} must be an object")
69
+ return value
70
+
71
+
72
+ def _positive_int(document: dict[str, Any], key: str, default: int) -> int:
73
+ value = document.get(key, default)
74
+ if not isinstance(value, int) or isinstance(value, bool) or value < 1:
75
+ raise NeutralityError(f"{key} must be a positive integer")
76
+ return value