status.py python
313 lines 9.9 KB
Raw
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439 feat: K1-P1 complete — agent provenance, build-verification… Sonnet 4.6 patch 57 days ago
1 """Honesty-status co-requirement check (§K9.8)."""
2
3 from __future__ import annotations
4
5 from dataclasses import dataclass
6 from pathlib import Path
7 from typing import Any
8
9 from adapters.config import OverseerConfig
10 from cli.paths import confine_path, repo_relative
11 from tools.honesty.artifact import sha256_file_bytes
12 from tools.honesty.gate import check_roles_file, honesty_module_disabled, hook_enabled
13 from tools.honesty.ledger_io import read_ledger_entries
14 from tools.honesty.provenance import (
15 provenance_has_signature,
16 signature_required_for_kind,
17 verify_entry_provenance,
18 )
19 from tools.honesty.types import HOOK_NAMES, HonestyStatusJson, HonestyStatusResult
20
21
22 @dataclass(frozen=True)
23 class HonestyStatusOptions:
24 """CLI options for honesty-status."""
25
26 hook: str | None
27 artifact: str | None
28 producer_session: str | None = None
29 emit_json: bool = False
30
31
32 def _has_l1_evidence(entry: dict[str, Any]) -> bool:
33 evidence = entry.get("evidence")
34 if not isinstance(evidence, dict):
35 return False
36 reexecuted = evidence.get("reexecuted")
37 if not isinstance(reexecuted, list):
38 return False
39 prefix = "verify-step:"
40 return any(isinstance(item, str) and item.startswith(prefix) for item in reexecuted)
41
42
43 def _match_verdicts(
44 entries: list[dict[str, Any]],
45 *,
46 artifact_sha256: str,
47 producer_session: str | None,
48 ) -> list[dict[str, Any]]:
49 matches: list[dict[str, Any]] = []
50 for entry in entries:
51 if entry.get("kind") != "verdict":
52 continue
53 if entry.get("passed") is not True:
54 continue
55 if entry.get("actor_role") != "verifier":
56 continue
57 if entry.get("artifact_sha256") != artifact_sha256:
58 continue
59 if producer_session is not None and entry.get("actor_session_id") == producer_session:
60 continue
61 matches.append(entry)
62 return matches
63
64
65 def run_honesty_status(
66 *,
67 config: OverseerConfig,
68 repo_root: Path,
69 options: HonestyStatusOptions,
70 ) -> HonestyStatusResult:
71 """Evaluate co-requirement for a hook + artifact pair."""
72 hook = options.hook
73 artifact = options.artifact
74
75 if not hook or not artifact:
76 payload = HonestyStatusJson(
77 ok=False,
78 exit_code=1,
79 hook=hook,
80 artifact=artifact,
81 producer_session=options.producer_session,
82 error="usage",
83 )
84 return HonestyStatusResult(exit_code=1, json_payload=payload)
85
86 if hook not in HOOK_NAMES:
87 payload = HonestyStatusJson(
88 ok=False,
89 exit_code=1,
90 hook=hook,
91 artifact=artifact,
92 producer_session=options.producer_session,
93 error="usage",
94 )
95 return HonestyStatusResult(exit_code=1, json_payload=payload)
96
97 if honesty_module_disabled(config):
98 payload = HonestyStatusJson(
99 ok=False,
100 exit_code=4,
101 hook=hook,
102 artifact=artifact,
103 producer_session=options.producer_session,
104 error="refused",
105 )
106 return HonestyStatusResult(
107 exit_code=4,
108 json_payload=payload,
109 stderr_extra="refused: honesty.enabled is false",
110 )
111
112 roles_exit, roles_warn = check_roles_file(config.honesty, repo_root)
113 if roles_exit is not None:
114 payload = HonestyStatusJson(
115 ok=False,
116 exit_code=4,
117 hook=hook,
118 artifact=artifact,
119 producer_session=options.producer_session,
120 error="refused",
121 )
122 return HonestyStatusResult(exit_code=4, json_payload=payload)
123
124 if not hook_enabled(config, hook):
125 payload = HonestyStatusJson(
126 ok=False,
127 exit_code=4,
128 hook=hook,
129 artifact=artifact,
130 producer_session=options.producer_session,
131 error="refused",
132 )
133 return HonestyStatusResult(
134 exit_code=4,
135 json_payload=payload,
136 stderr_extra=f"refused: hook {hook!r} not enabled for co-requirement",
137 )
138
139 try:
140 ledger_rel = config.honesty.ledger
141 if ledger_rel is None or not ledger_rel.strip():
142 raise ValueError("missing ledger")
143 ledger_path = confine_path(repo_root, ledger_rel)
144 except Exception:
145 payload = HonestyStatusJson(
146 ok=False,
147 exit_code=4,
148 hook=hook,
149 artifact=artifact,
150 producer_session=options.producer_session,
151 error="refused",
152 )
153 return HonestyStatusResult(exit_code=4, json_payload=payload)
154
155 try:
156 artifact_path = confine_path(repo_root, artifact)
157 except Exception:
158 payload = HonestyStatusJson(
159 ok=False,
160 exit_code=4,
161 hook=hook,
162 artifact=artifact,
163 producer_session=options.producer_session,
164 error="refused",
165 )
166 return HonestyStatusResult(exit_code=4, json_payload=payload)
167
168 if not artifact_path.is_file():
169 payload = HonestyStatusJson(
170 ok=False,
171 exit_code=4,
172 hook=hook,
173 artifact=artifact,
174 producer_session=options.producer_session,
175 error="refused",
176 )
177 return HonestyStatusResult(exit_code=4, json_payload=payload)
178
179 artifact_rel = repo_relative(repo_root, artifact_path)
180 try:
181 artifact_sha256 = sha256_file_bytes(artifact_path)
182 except OSError:
183 payload = HonestyStatusJson(
184 ok=False,
185 exit_code=4,
186 hook=hook,
187 artifact=artifact_rel,
188 producer_session=options.producer_session,
189 error="refused",
190 )
191 return HonestyStatusResult(exit_code=4, json_payload=payload)
192
193 if not ledger_path.is_file() or ledger_path.stat().st_size == 0:
194 payload = HonestyStatusJson(
195 ok=False,
196 exit_code=20,
197 hook=hook,
198 artifact=artifact_rel,
199 artifact_sha256=artifact_sha256,
200 producer_session=options.producer_session,
201 matched_verdict_hash=None,
202 error="missing_verdict",
203 )
204 stderr = roles_warn or ""
205 return HonestyStatusResult(exit_code=20, json_payload=payload, stderr_extra=stderr)
206
207 try:
208 entries = read_ledger_entries(ledger_path)
209 except (ValueError, OSError):
210 payload = HonestyStatusJson(
211 ok=False,
212 exit_code=4,
213 hook=hook,
214 artifact=artifact_rel,
215 artifact_sha256=artifact_sha256,
216 producer_session=options.producer_session,
217 error="refused",
218 )
219 return HonestyStatusResult(exit_code=4, json_payload=payload)
220
221 matches = _match_verdicts(
222 entries,
223 artifact_sha256=artifact_sha256,
224 producer_session=options.producer_session,
225 )
226
227 if not matches:
228 payload = HonestyStatusJson(
229 ok=False,
230 exit_code=20,
231 hook=hook,
232 artifact=artifact_rel,
233 artifact_sha256=artifact_sha256,
234 producer_session=options.producer_session,
235 matched_verdict_hash=None,
236 error="missing_verdict",
237 )
238 return HonestyStatusResult(exit_code=20, json_payload=payload, stderr_extra=roles_warn or "")
239
240 winner = matches[-1]
241 matched_hash = winner.get("entry_hash")
242
243 if signature_required_for_kind(
244 require_agent_signature=config.honesty.require_agent_signature,
245 kind=str(winner.get("kind", "")),
246 ) and not provenance_has_signature(winner):
247 payload = HonestyStatusJson(
248 ok=False,
249 exit_code=26,
250 hook=hook,
251 artifact=artifact_rel,
252 artifact_sha256=artifact_sha256,
253 producer_session=options.producer_session,
254 matched_verdict_hash=matched_hash if isinstance(matched_hash, str) else None,
255 error="refused",
256 )
257 return HonestyStatusResult(
258 exit_code=26,
259 json_payload=payload,
260 stderr_extra=roles_warn or "",
261 )
262
263 sig_code = verify_entry_provenance(winner, regime=config.vcs.regime)
264 if sig_code != 0:
265 payload = HonestyStatusJson(
266 ok=False,
267 exit_code=sig_code,
268 hook=hook,
269 artifact=artifact_rel,
270 artifact_sha256=artifact_sha256,
271 producer_session=options.producer_session,
272 matched_verdict_hash=matched_hash if isinstance(matched_hash, str) else None,
273 error="refused",
274 )
275 return HonestyStatusResult(
276 exit_code=sig_code,
277 json_payload=payload,
278 stderr_extra=roles_warn or "",
279 )
280
281 mode = config.honesty.require_l1_evidence
282 warn_msg = ""
283 if mode in {"warn", "require"} and not _has_l1_evidence(winner):
284 if mode == "require":
285 payload = HonestyStatusJson(
286 ok=False,
287 exit_code=20,
288 hook=hook,
289 artifact=artifact_rel,
290 artifact_sha256=artifact_sha256,
291 producer_session=options.producer_session,
292 matched_verdict_hash=matched_hash if isinstance(matched_hash, str) else None,
293 error="missing_verdict",
294 )
295 return HonestyStatusResult(exit_code=20, json_payload=payload, stderr_extra=roles_warn or "")
296 warn_msg = "warning: matched verdict lacks verify-step: L1 evidence"
297
298 stderr_parts = [part for part in (roles_warn, warn_msg) if part]
299 payload = HonestyStatusJson(
300 ok=True,
301 exit_code=0,
302 hook=hook,
303 artifact=artifact_rel,
304 artifact_sha256=artifact_sha256,
305 producer_session=options.producer_session,
306 matched_verdict_hash=matched_hash if isinstance(matched_hash, str) else None,
307 error=None,
308 )
309 return HonestyStatusResult(
310 exit_code=0,
311 json_payload=payload,
312 stderr_extra="\n".join(stderr_parts),
313 )
File History 1 commit
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439 feat: K1-P1 complete — agent provenance, build-verification… Sonnet 4.6 patch 57 days ago