governed-agent 0.1.3__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.
@@ -0,0 +1,72 @@
1
+ """Governed Agent — capability sealing, scope-bounded planning, independent audit, and runtime tool boundaries."""
2
+
3
+ from .capability import (
4
+ KeyPair,
5
+ SealResult,
6
+ VerifyResult,
7
+ generate_keypair,
8
+ seal,
9
+ verify,
10
+ quick_seal,
11
+ quick_verify,
12
+ FileKeyStore,
13
+ create_keystore,
14
+ Pin,
15
+ Checkpoint,
16
+ FileBackedAuthority,
17
+ InMemoryAuthority,
18
+ create_antirollback_authority,
19
+ create_checkpoint,
20
+ )
21
+ from .planning import (
22
+ validate,
23
+ render_brief,
24
+ create_handoff,
25
+ ALLOWED_ACTIONS,
26
+ HIGH_RISK_ACTIONS,
27
+ REQUIRED_PROHIBITIONS,
28
+ enabled,
29
+ )
30
+ from .audit import (
31
+ WatchTower,
32
+ WatchTowerVerdict,
33
+ )
34
+ from .hermes import GovernedToolBoundary, ExecutionDeniedError
35
+
36
+ __version__ = "0.1.2"
37
+ __author__ = "jtevesnz-sys"
38
+ __license__ = "MIT"
39
+
40
+ __all__ = [
41
+ # Capability Layer
42
+ "KeyPair",
43
+ "SealResult",
44
+ "VerifyResult",
45
+ "generate_keypair",
46
+ "seal",
47
+ "verify",
48
+ "quick_seal",
49
+ "quick_verify",
50
+ "FileKeyStore",
51
+ "create_keystore",
52
+ "Pin",
53
+ "Checkpoint",
54
+ "FileBackedAuthority",
55
+ "InMemoryAuthority",
56
+ "create_antirollback_authority",
57
+ "create_checkpoint",
58
+ # Planning Layer
59
+ "validate",
60
+ "render_brief",
61
+ "create_handoff",
62
+ "ALLOWED_ACTIONS",
63
+ "HIGH_RISK_ACTIONS",
64
+ "REQUIRED_PROHIBITIONS",
65
+ "enabled",
66
+ # Audit Layer
67
+ "WatchTower",
68
+ "WatchTowerVerdict",
69
+ # Hermes Tool Boundary
70
+ "GovernedToolBoundary",
71
+ "ExecutionDeniedError",
72
+ ]
@@ -0,0 +1,5 @@
1
+ """Audit layer: independent 6-point verification and verdict engine."""
2
+
3
+ from .watchtower import WatchTower, WatchTowerVerdict
4
+
5
+ __all__ = ["WatchTower", "WatchTowerVerdict"]
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Governed Agent — Independent Verifier (Watchtower Audit Engine)
4
+ Performs out-of-band 6-point verification over build outputs and manifests.
5
+ Prevents models from self-certifying their work.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+ import hashlib
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional, Set, Tuple, Union
15
+
16
+
17
+ class WatchTowerVerdict(Enum):
18
+ """Independent audit verdicts."""
19
+
20
+ PASS = "PASS"
21
+ HOLD = "HOLD"
22
+ REVISE = "REVISE"
23
+
24
+
25
+ class WatchTower:
26
+ """Out-of-band auditor validating artifacts against manifests and contracts."""
27
+
28
+ def __init__(self, root_dir: Union[str, Path, None] = None):
29
+ self.root_dir = Path(root_dir) if root_dir else None
30
+
31
+ def verify(
32
+ self,
33
+ manifest: Union[Dict[str, Any], Path, str],
34
+ dep_graph: Optional[Dict[str, Any]] = None,
35
+ lane_contracts: Optional[List[Dict[str, Any]]] = None,
36
+ root_dir: Union[str, Path, None] = None,
37
+ ) -> Dict[str, Any]:
38
+ """
39
+ Execute 6-point independent audit:
40
+ 1. Manifest validation
41
+ 2. Required lanes present
42
+ 3. No unlisted files
43
+ 4. Hash agreement
44
+ 5. Cross-lane independence check
45
+ 6. Prohibited residue detection
46
+ """
47
+ dep_graph = dep_graph or {}
48
+ lane_contracts = lane_contracts or []
49
+ effective_root = Path(root_dir) if root_dir else self.root_dir
50
+
51
+ if isinstance(manifest, (Path, str)):
52
+ manifest_path = Path(manifest)
53
+ manifest_dict = self._load_json(manifest_path)
54
+ if effective_root is None:
55
+ effective_root = manifest_path.parent
56
+ else:
57
+ manifest_dict = manifest
58
+
59
+ checks: List[Dict[str, Any]] = []
60
+ independence_violations: List[str] = []
61
+
62
+ # 1. Manifest structure validation
63
+ checks.append(self._check_manifest_valid(manifest_dict))
64
+
65
+ # Complete set hash check if present and valid
66
+ checks.append(self._check_complete_set_hash(manifest_dict))
67
+
68
+ # 2. Required lanes present
69
+ checks.append(self._check_lanes_present(manifest_dict, dep_graph))
70
+
71
+ # 3. No unlisted files
72
+ checks.append(self._check_no_unlisted_files(manifest_dict, effective_root))
73
+
74
+ # 4. Hash agreement
75
+ checks.append(self._check_hashes(manifest_dict, effective_root))
76
+
77
+ # 5. Independence check
78
+ ind_check, violations = self._check_independence(manifest_dict, lane_contracts)
79
+ checks.append(ind_check)
80
+ independence_violations.extend(violations)
81
+
82
+ # 6. No residue
83
+ residue_check, residue_found = self._check_residue(manifest_dict)
84
+ checks.append(residue_check)
85
+
86
+ # Determine verdict
87
+ verdict = self._determine_verdict(checks, independence_violations, residue_found)
88
+
89
+ return {
90
+ "verdict": verdict.value,
91
+ "checks": checks,
92
+ "independence_violations": independence_violations,
93
+ "prohibited_residue_found": residue_found,
94
+ }
95
+
96
+ def _load_json(self, path: Path) -> Dict[str, Any]:
97
+ with open(path, "r", encoding="utf-8") as f:
98
+ return json.load(f)
99
+
100
+ def _check_complete_set_hash(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
101
+ complete_set_sha = manifest.get("complete_set_sha256")
102
+ if not complete_set_sha or not isinstance(complete_set_sha, str) or len(complete_set_sha) != 64:
103
+ return {
104
+ "name": "complete_set_sha256",
105
+ "status": "FAIL",
106
+ "details": "Invalid complete_set_sha256 in manifest",
107
+ }
108
+ files = manifest.get("files", [])
109
+ if not isinstance(files, list):
110
+ return {
111
+ "name": "complete_set_sha256",
112
+ "status": "FAIL",
113
+ "details": "'files' must be a list",
114
+ }
115
+
116
+ try:
117
+ # Deterministic canonical serialization of file entries
118
+ normalized = []
119
+ for f in files:
120
+ rel = f.get("relative_path", "").replace("\\", "/")
121
+ entry: Dict[str, Any] = {
122
+ "lane_id": f.get("lane_id", ""),
123
+ "relative_path": rel,
124
+ "sha256": f.get("sha256", ""),
125
+ }
126
+ if "size_bytes" in f:
127
+ entry["size_bytes"] = f["size_bytes"]
128
+ normalized.append(entry)
129
+
130
+ normalized.sort(key=lambda x: x["relative_path"])
131
+ canonical = json.dumps(normalized, separators=(",", ":"), sort_keys=True).encode("utf-8")
132
+ recomputed = hashlib.sha256(canonical).hexdigest()
133
+
134
+ if recomputed.lower() != complete_set_sha.lower():
135
+ return {
136
+ "name": "complete_set_sha256",
137
+ "status": "FAIL",
138
+ "details": f"complete_set_sha256 mismatch: expected {complete_set_sha}, computed {recomputed}",
139
+ }
140
+ except Exception as exc:
141
+ return {
142
+ "name": "complete_set_sha256",
143
+ "status": "FAIL",
144
+ "details": f"Error computing complete_set_sha256: {exc}",
145
+ }
146
+
147
+ return {
148
+ "name": "complete_set_sha256",
149
+ "status": "PASS",
150
+ "details": "complete_set_sha256 verified",
151
+ }
152
+
153
+ def _check_manifest_valid(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
154
+ required = ["format", "format_version", "convergence_id", "complete_set_sha256", "files", "lane_records"]
155
+ missing = [f for f in required if f not in manifest]
156
+ status = "PASS" if not missing else "FAIL"
157
+ return {
158
+ "name": "manifest_validation",
159
+ "status": status,
160
+ "details": f"Missing: {missing}" if missing else "All required manifest fields present",
161
+ }
162
+
163
+ def _check_lanes_present(self, manifest: Dict[str, Any], dep_graph: Dict[str, Any]) -> Dict[str, Any]:
164
+ expected = {n["lane_id"] for n in dep_graph.get("nodes", []) if n.get("node_type") == "LANE"}
165
+ actual = {lr["lane_id"] for lr in manifest.get("lane_records", []) if "lane_id" in lr}
166
+ missing = expected - actual
167
+ status = "PASS" if not missing else "FAIL"
168
+ return {
169
+ "name": "required_lanes",
170
+ "status": status,
171
+ "details": f"Missing lanes: {sorted(missing)}" if missing else "All required lanes present",
172
+ }
173
+
174
+ def _check_no_unlisted_files(
175
+ self,
176
+ manifest: Dict[str, Any],
177
+ root_dir: Optional[Path] = None,
178
+ ) -> Dict[str, Any]:
179
+ # Verifies files list format and detects unlisted files on disk
180
+ files = manifest.get("files", [])
181
+ if not isinstance(files, list):
182
+ return {"name": "no_unlisted_files", "status": "FAIL", "details": "'files' must be a list"}
183
+
184
+ target_root = root_dir if root_dir is not None else self.root_dir
185
+ if target_root is None or not target_root.is_dir():
186
+ return {"name": "no_unlisted_files", "status": "FAIL", "details": "Artifact root directory is required"}
187
+
188
+ manifest_paths = set()
189
+ for f in files:
190
+ rel = f.get("relative_path")
191
+ if rel:
192
+ manifest_paths.add(Path(rel).as_posix())
193
+
194
+ unlisted = []
195
+ for p in target_root.rglob("*"):
196
+ if p.is_file():
197
+ rel_posix = p.relative_to(target_root).as_posix()
198
+ # Skip ignore files / directories or manifest itself if located in root
199
+ if rel_posix in {".git", ".gitignore", "manifest.json"}:
200
+ continue
201
+ if rel_posix not in manifest_paths:
202
+ unlisted.append(rel_posix)
203
+
204
+ if unlisted:
205
+ return {
206
+ "name": "no_unlisted_files",
207
+ "status": "FAIL",
208
+ "details": f"Unlisted files detected: {sorted(unlisted)}",
209
+ }
210
+ return {"name": "no_unlisted_files", "status": "PASS", "details": "No unlisted files detected"}
211
+
212
+ def _check_hashes(
213
+ self,
214
+ manifest: Dict[str, Any],
215
+ root_dir: Optional[Path] = None,
216
+ ) -> Dict[str, Any]:
217
+ files = manifest.get("files", [])
218
+ target_root = root_dir if root_dir is not None else self.root_dir
219
+ if target_root is None or not target_root.is_dir():
220
+ return {"name": "hash_agreement", "status": "FAIL", "details": "Artifact root directory is required"}
221
+ if not isinstance(files, list) or not files:
222
+ return {"name": "hash_agreement", "status": "FAIL", "details": "At least one artifact file is required"}
223
+
224
+ for f in files:
225
+ sha = f.get("sha256", "")
226
+ rel = f.get("relative_path", "")
227
+ if not isinstance(sha, str) or len(sha) != 64:
228
+ return {
229
+ "name": "hash_agreement",
230
+ "status": "FAIL",
231
+ "details": f"Invalid sha256 for file: {rel}",
232
+ }
233
+ if "size_bytes" in f and (type(f["size_bytes"]) is not int or f["size_bytes"] < 0):
234
+ return {"name": "hash_agreement", "status": "FAIL", "details": f"Invalid size_bytes for file: {rel}"}
235
+
236
+ if not target_root.is_dir():
237
+ return {"name": "hash_agreement", "status": "FAIL", "details": "Artifact root directory is unavailable"}
238
+ file_path = (target_root / rel).resolve()
239
+ try:
240
+ # Canonical containment check: prevent path traversal outside root_dir
241
+ file_path.relative_to(target_root.resolve())
242
+ except ValueError:
243
+ return {
244
+ "name": "hash_agreement",
245
+ "status": "FAIL",
246
+ "details": f"Path traversal escape detected for file: {rel}",
247
+ }
248
+
249
+ if not file_path.is_file():
250
+ return {
251
+ "name": "hash_agreement",
252
+ "status": "FAIL",
253
+ "details": f"Missing file on disk: {rel}",
254
+ }
255
+
256
+ h = hashlib.sha256()
257
+ actual_size = 0
258
+ with open(file_path, "rb") as fp:
259
+ for chunk in iter(lambda: fp.read(8192), b""):
260
+ actual_size += len(chunk)
261
+ h.update(chunk)
262
+ if "size_bytes" in f and actual_size != f["size_bytes"]:
263
+ return {
264
+ "name": "hash_agreement",
265
+ "status": "FAIL",
266
+ "details": f"Size mismatch for {rel}: expected {f['size_bytes']}, got {actual_size}",
267
+ }
268
+ computed = h.hexdigest()
269
+ if computed.lower() != sha.lower():
270
+ return {
271
+ "name": "hash_agreement",
272
+ "status": "FAIL",
273
+ "details": f"Hash mismatch for {rel}: expected {sha}, got {computed}",
274
+ }
275
+
276
+ return {"name": "hash_agreement", "status": "PASS", "details": "All listed hashes verified"}
277
+
278
+ def _check_independence(
279
+ self,
280
+ manifest: Dict[str, Any],
281
+ lane_contracts: List[Dict[str, Any]],
282
+ ) -> Tuple[Dict[str, Any], List[str]]:
283
+ groups: Dict[str, List[str]] = {}
284
+ for contract in lane_contracts:
285
+ ig = contract.get("independence_group")
286
+ if ig:
287
+ groups.setdefault(ig, []).append(contract["lane_id"])
288
+
289
+ violations: List[str] = []
290
+ for group_id, lane_ids in groups.items():
291
+ if len(lane_ids) > 1:
292
+ files = manifest.get("files", [])
293
+ for f in files:
294
+ if f.get("lane_id") in lane_ids:
295
+ path = f.get("relative_path")
296
+ for other_lane in lane_ids:
297
+ if other_lane != f.get("lane_id"):
298
+ other_file = next(
299
+ (
300
+ f2
301
+ for f2 in files
302
+ if f2.get("relative_path") == path and f2.get("lane_id") == other_lane
303
+ ),
304
+ None,
305
+ )
306
+ if other_file and other_file.get("sha256") != f.get("sha256"):
307
+ violations.append(
308
+ f"Independence collision: {f.get('lane_id')} vs {other_lane} on {path}"
309
+ )
310
+
311
+ status = "PASS" if not violations else "FAIL"
312
+ return (
313
+ {
314
+ "name": "independence",
315
+ "status": status,
316
+ "details": f"Violations: {violations}" if violations else "Independence verified",
317
+ },
318
+ violations,
319
+ )
320
+
321
+ def _check_residue(self, manifest: Dict[str, Any]) -> Tuple[Dict[str, Any], bool]:
322
+ prohibited_residue = False
323
+ files = manifest.get("files", [])
324
+ for f in files:
325
+ path = str(f.get("relative_path", "")).lower()
326
+ if any(term in path for term in [".tmp", ".bak", "desktop.ini", ".ds_store"]):
327
+ prohibited_residue = True
328
+ break
329
+
330
+ status = "HOLD" if prohibited_residue else "PASS"
331
+ return (
332
+ {
333
+ "name": "no_residue",
334
+ "status": status,
335
+ "details": "Prohibited residue found" if prohibited_residue else "No residue detected",
336
+ },
337
+ prohibited_residue,
338
+ )
339
+
340
+ def _determine_verdict(
341
+ self,
342
+ checks: List[Dict[str, Any]],
343
+ ind_violations: List[str],
344
+ residue_found: bool,
345
+ ) -> WatchTowerVerdict:
346
+ if ind_violations:
347
+ return WatchTowerVerdict.REVISE
348
+
349
+ mandatory = ["manifest_validation", "required_lanes", "no_unlisted_files", "hash_agreement", "complete_set_sha256"]
350
+ for c in checks:
351
+ if c["name"] in mandatory and c["status"] == "FAIL":
352
+ if c["name"] in {"hash_agreement", "complete_set_sha256"}:
353
+ if c["name"] == "hash_agreement" and "Missing file on disk" in c.get("details", ""):
354
+ return WatchTowerVerdict.HOLD
355
+ return WatchTowerVerdict.REVISE
356
+ return WatchTowerVerdict.HOLD
357
+
358
+ if residue_found:
359
+ return WatchTowerVerdict.HOLD
360
+
361
+ return WatchTowerVerdict.PASS
@@ -0,0 +1,57 @@
1
+ """Capability layer: cryptographic sealing and anti-rollback authority."""
2
+
3
+ from .crypto import (
4
+ KeySize,
5
+ KeyExponent,
6
+ KeyPair,
7
+ SealResult,
8
+ VerifyResult,
9
+ KeyStore,
10
+ FileKeyStore,
11
+ compute_sha256,
12
+ compute_fingerprint_sha256,
13
+ generate_keypair,
14
+ canonicalize_json,
15
+ compute_canonical_hash,
16
+ seal,
17
+ verify,
18
+ create_keystore,
19
+ quick_seal,
20
+ quick_verify,
21
+ )
22
+ from .antirollback import (
23
+ AntirollbackAuthority,
24
+ Pin,
25
+ Checkpoint,
26
+ FileBackedAuthority,
27
+ InMemoryAuthority,
28
+ create_antirollback_authority,
29
+ create_checkpoint,
30
+ )
31
+
32
+ __all__ = [
33
+ "KeySize",
34
+ "KeyExponent",
35
+ "KeyPair",
36
+ "SealResult",
37
+ "VerifyResult",
38
+ "KeyStore",
39
+ "FileKeyStore",
40
+ "compute_sha256",
41
+ "compute_fingerprint_sha256",
42
+ "generate_keypair",
43
+ "canonicalize_json",
44
+ "compute_canonical_hash",
45
+ "seal",
46
+ "verify",
47
+ "create_keystore",
48
+ "quick_seal",
49
+ "quick_verify",
50
+ "AntirollbackAuthority",
51
+ "Pin",
52
+ "Checkpoint",
53
+ "FileBackedAuthority",
54
+ "InMemoryAuthority",
55
+ "create_antirollback_authority",
56
+ "create_checkpoint",
57
+ ]