cfa-kernel 0.1.0__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.
- cfa/__init__.py +39 -0
- cfa/_lazy.py +39 -0
- cfa/adapters/__init__.py +104 -0
- cfa/adapters/autogen.py +19 -0
- cfa/adapters/crewai.py +19 -0
- cfa/adapters/dspy.py +19 -0
- cfa/adapters/langgraph.py +19 -0
- cfa/adapters/openai_agents.py +19 -0
- cfa/audit/__init__.py +15 -0
- cfa/audit/context.py +205 -0
- cfa/audit/hashing.py +41 -0
- cfa/audit/trail.py +194 -0
- cfa/backends/__init__.py +132 -0
- cfa/backends/dbt.py +338 -0
- cfa/backends/pyspark.py +240 -0
- cfa/backends/sql.py +270 -0
- cfa/behavior/__init__.py +49 -0
- cfa/behavior/llm.py +244 -0
- cfa/behavior/spec.py +235 -0
- cfa/behavior/systematizer.py +222 -0
- cfa/cli/__init__.py +296 -0
- cfa/cli/__main__.py +6 -0
- cfa/cli/_helpers.py +109 -0
- cfa/cli/core/__init__.py +0 -0
- cfa/cli/core/evaluate.py +72 -0
- cfa/cli/core/validate.py +29 -0
- cfa/cli/formatters.py +280 -0
- cfa/cli/governance/__init__.py +0 -0
- cfa/cli/governance/audit.py +65 -0
- cfa/cli/governance/catalog.py +28 -0
- cfa/cli/governance/policy.py +119 -0
- cfa/cli/governance/rules.py +42 -0
- cfa/cli/governance/signature.py +31 -0
- cfa/cli/infrastructure/__init__.py +0 -0
- cfa/cli/infrastructure/backend_list.py +24 -0
- cfa/cli/infrastructure/storage.py +87 -0
- cfa/cli/project/__init__.py +0 -0
- cfa/cli/project/init.py +73 -0
- cfa/cli/project/lifecycle.py +92 -0
- cfa/cli/project/status.py +75 -0
- cfa/cli/project/taxonomy.py +38 -0
- cfa/cli/reporting/__init__.py +0 -0
- cfa/cli/reporting/report.py +109 -0
- cfa/cli/reporting/serve.py +43 -0
- cfa/config.py +103 -0
- cfa/core/__init__.py +19 -0
- cfa/core/codegen.py +65 -0
- cfa/core/conditions.py +129 -0
- cfa/core/kernel.py +224 -0
- cfa/core/phases/__init__.py +0 -0
- cfa/core/phases/runner.py +477 -0
- cfa/core/planner.py +290 -0
- cfa/execution/__init__.py +12 -0
- cfa/execution/partial.py +339 -0
- cfa/execution/state_projection.py +216 -0
- cfa/governance/__init__.py +76 -0
- cfa/lifecycle/__init__.py +51 -0
- cfa/mcp/__init__.py +347 -0
- cfa/mcp/__main__.py +4 -0
- cfa/normalizer/__init__.py +15 -0
- cfa/normalizer/base.py +441 -0
- cfa/normalizer/llm.py +426 -0
- cfa/observability/__init__.py +14 -0
- cfa/observability/indices.py +177 -0
- cfa/observability/metrics.py +91 -0
- cfa/observability/notify.py +79 -0
- cfa/observability/otel.py +81 -0
- cfa/observability/promotion.py +367 -0
- cfa/policy/__init__.py +12 -0
- cfa/policy/bundle.py +317 -0
- cfa/policy/catalog.py +117 -0
- cfa/policy/engine.py +306 -0
- cfa/reporting/__init__.py +42 -0
- cfa/reporting/charts.py +223 -0
- cfa/reporting/engine.py +456 -0
- cfa/resolution/__init__.py +62 -0
- cfa/runtime/__init__.py +13 -0
- cfa/runtime/gate.py +287 -0
- cfa/sandbox/__init__.py +189 -0
- cfa/sandbox/executor.py +92 -0
- cfa/sandbox/mock.py +89 -0
- cfa/sandbox/panic.py +52 -0
- cfa/storage/__init__.py +591 -0
- cfa/testing/__init__.py +60 -0
- cfa/testing/asserts.py +77 -0
- cfa/testing/evaluate.py +168 -0
- cfa/testing/fixtures.py +89 -0
- cfa/testing/markers.py +36 -0
- cfa/types.py +489 -0
- cfa/validation/__init__.py +14 -0
- cfa/validation/runtime.py +285 -0
- cfa/validation/signature.py +146 -0
- cfa/validation/static.py +252 -0
- cfa_kernel-0.1.0.dist-info/METADATA +32 -0
- cfa_kernel-0.1.0.dist-info/RECORD +98 -0
- cfa_kernel-0.1.0.dist-info/WHEEL +4 -0
- cfa_kernel-0.1.0.dist-info/entry_points.txt +3 -0
- cfa_kernel-0.1.0.dist-info/licenses/LICENSE +21 -0
cfa/cli/formatters.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CFA CLI — formatters
|
|
3
|
+
====================
|
|
4
|
+
Output formatters for CLI results: table, JSON, summary.
|
|
5
|
+
Zero external dependencies — uses only stdlib.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
|
16
|
+
|
|
17
|
+
# Force UTF-8 on Windows if possible
|
|
18
|
+
if sys.platform == "win32":
|
|
19
|
+
try:
|
|
20
|
+
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
# ANSI color codes (only when stdout is a TTY)
|
|
25
|
+
_RESET = "\033[0m"
|
|
26
|
+
_BOLD = "\033[1m"
|
|
27
|
+
_DIM = "\033[2m"
|
|
28
|
+
_RED = "\033[31m"
|
|
29
|
+
_GREEN = "\033[32m"
|
|
30
|
+
_YELLOW = "\033[33m"
|
|
31
|
+
_BLUE = "\033[34m"
|
|
32
|
+
_CYAN = "\033[36m"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _use_color() -> bool:
|
|
36
|
+
return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _c(code: str, text: str) -> str:
|
|
40
|
+
return f"{code}{text}{_RESET}" if _use_color() else text
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_ICONS_UTF8 = {
|
|
44
|
+
"approved": "✓", "approved_with_warnings": "⚠", "blocked": "✗",
|
|
45
|
+
"replanned": "↻", "rolled_back": "↩", "quarantined": "⊘",
|
|
46
|
+
"partially_committed": "◐", "promotion_candidate": "★",
|
|
47
|
+
}
|
|
48
|
+
_ICONS_ASCII = {
|
|
49
|
+
"approved": "[OK]", "approved_with_warnings": "[!!]", "blocked": "[XX]",
|
|
50
|
+
"replanned": "[>>]", "rolled_back": "[<<]", "quarantined": "[??]",
|
|
51
|
+
"partially_committed": "[~]", "promotion_candidate": "[**]",
|
|
52
|
+
}
|
|
53
|
+
_BOX_UTF8 = {"tl": "┌", "tr": "┐", "bl": "└", "br": "┘", "h": "─", "v": "│", "ml": "├", "mr": "┤", "x": "┼"}
|
|
54
|
+
_BOX_ASCII = {"tl": "+", "tr": "+", "bl": "+", "br": "+", "h": "-", "v": "|", "ml": "+", "mr": "+", "x": "+"}
|
|
55
|
+
|
|
56
|
+
_UTF8_OK = True
|
|
57
|
+
try:
|
|
58
|
+
"┌─✓".encode(sys.stdout.encoding or "ascii")
|
|
59
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
60
|
+
_UTF8_OK = False
|
|
61
|
+
|
|
62
|
+
# Force ASCII on Windows unless explicitly using UTF-8 terminal
|
|
63
|
+
if sys.platform == "win32" and (sys.stdout.encoding or "").lower() not in ("utf-8", "utf8"):
|
|
64
|
+
_UTF8_OK = False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _visible_len(text: str) -> int:
|
|
68
|
+
"""String length excluding ANSI color codes."""
|
|
69
|
+
return len(_ANSI_RE.sub("", text))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _pad_right(text: str, width: int) -> str:
|
|
73
|
+
"""Pad text to visible width, accounting for ANSI codes."""
|
|
74
|
+
visible = _visible_len(text)
|
|
75
|
+
if visible >= width:
|
|
76
|
+
return text
|
|
77
|
+
return text + " " * (width - visible)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _pad_center(text: str, width: int, fillchar: str = " ") -> str:
|
|
81
|
+
"""Center text accounting for ANSI codes."""
|
|
82
|
+
visible = _visible_len(text)
|
|
83
|
+
if visible >= width:
|
|
84
|
+
return text
|
|
85
|
+
left = (width - visible) // 2
|
|
86
|
+
right = width - visible - left
|
|
87
|
+
return (fillchar * left) + text + (fillchar * right)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _icon(state: str) -> str:
|
|
91
|
+
icons = _ICONS_UTF8 if _UTF8_OK else _ICONS_ASCII
|
|
92
|
+
return icons.get(state, "?")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _box(key: str) -> str:
|
|
96
|
+
b = _BOX_UTF8 if _UTF8_OK else _BOX_ASCII
|
|
97
|
+
return b.get(key, key)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _status_icon(state: str) -> str:
|
|
101
|
+
icon = _icon(state)
|
|
102
|
+
colors = {
|
|
103
|
+
"✓": _GREEN, "[OK]": _GREEN,
|
|
104
|
+
"⚠": _YELLOW, "[!!]": _YELLOW,
|
|
105
|
+
"✗": _RED, "[XX]": _RED,
|
|
106
|
+
"↻": _YELLOW, "[>>]": _YELLOW,
|
|
107
|
+
"↩": _RED, "[<<]": _RED,
|
|
108
|
+
"⊘": _YELLOW, "[??]": _YELLOW,
|
|
109
|
+
"◐": _YELLOW, "[~]": _YELLOW,
|
|
110
|
+
"★": _GREEN, "[**]": _GREEN,
|
|
111
|
+
}
|
|
112
|
+
code = colors.get(icon, "")
|
|
113
|
+
return _c(code, icon)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _severity_color(severity: str) -> str:
|
|
117
|
+
return {
|
|
118
|
+
"critical": _c(_RED, severity.upper()),
|
|
119
|
+
"high": _c(_RED, severity.upper()),
|
|
120
|
+
"warning": _c(_YELLOW, severity.upper()),
|
|
121
|
+
"info": _c(_BLUE, severity.upper()),
|
|
122
|
+
}.get(severity, severity.upper())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _table_line(widths: list[int], cells: list[str], pad: int = 1) -> str:
|
|
126
|
+
parts: list[str] = []
|
|
127
|
+
v = _box("v")
|
|
128
|
+
for w, cell in zip(widths, cells, strict=False):
|
|
129
|
+
if w > 2:
|
|
130
|
+
inner = " " + _pad_right(cell, w - 2) + " "
|
|
131
|
+
else:
|
|
132
|
+
inner = cell
|
|
133
|
+
parts.append(inner)
|
|
134
|
+
return v + v.join(parts) + v
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _table_sep(widths: list[int], left: str | None = None, mid: str | None = None, right: str | None = None) -> str:
|
|
138
|
+
left = left or _box("ml")
|
|
139
|
+
mid = mid or _box("x")
|
|
140
|
+
right = right or _box("mr")
|
|
141
|
+
parts = [left]
|
|
142
|
+
for i, w in enumerate(widths):
|
|
143
|
+
parts.append(_box("h") * w)
|
|
144
|
+
if i < len(widths) - 1:
|
|
145
|
+
parts.append(mid)
|
|
146
|
+
parts.append(right)
|
|
147
|
+
return "".join(parts)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def format_evaluate_table(result: dict[str, Any], faults: list[dict[str, Any]]) -> str:
|
|
151
|
+
"""Format an evaluate result as a bordered table."""
|
|
152
|
+
state = result.get("state", "unknown")
|
|
153
|
+
lines: list[str] = []
|
|
154
|
+
h = _box("h")
|
|
155
|
+
tl, tr, bl, br = _box("tl"), _box("tr"), _box("bl"), _box("br")
|
|
156
|
+
|
|
157
|
+
width = 60
|
|
158
|
+
title = " CFA Evaluation Result "
|
|
159
|
+
lines.append(f"{tl}{_pad_center(title, width - 2, h)}{tr}")
|
|
160
|
+
lines.append(_table_line([width], [""]))
|
|
161
|
+
lines.append(_table_line([width], [f"Intent: {result.get('intent', '')[:width - 12]}"]))
|
|
162
|
+
icon = _status_icon(state)
|
|
163
|
+
lines.append(_table_line([width], [f"State: {icon} {state}"]))
|
|
164
|
+
hash_val = result.get("signature_hash", "")
|
|
165
|
+
if hash_val:
|
|
166
|
+
lines.append(_table_line([width], [f"Hash: {hash_val}"]))
|
|
167
|
+
lines.append(_table_line([width], [f"Policy: {result.get('policy_bundle', '')} | Replans: {result.get('replan_count', 0)}"]))
|
|
168
|
+
lines.append(_table_line([width], [""]))
|
|
169
|
+
|
|
170
|
+
if faults:
|
|
171
|
+
lines.append(_table_line([width], [_c(_BOLD, "Faults")]))
|
|
172
|
+
lines.append(_table_line([width], [""]))
|
|
173
|
+
for f in faults:
|
|
174
|
+
sev = _severity_color(f.get("severity", "high"))
|
|
175
|
+
code = f.get("code", "")
|
|
176
|
+
msg = f.get("message", "")
|
|
177
|
+
lines.append(_table_line([width], [f" {sev} {code}"]))
|
|
178
|
+
lines.append(_table_line([width], [f" {msg[:width - 10]}"]))
|
|
179
|
+
remediation = f.get("remediation", [])
|
|
180
|
+
if remediation:
|
|
181
|
+
for i, r in enumerate(remediation[:3]):
|
|
182
|
+
lines.append(_table_line([width], [f" {_c(_DIM, f'{i+1}. {r[:width - 12]}')}"]))
|
|
183
|
+
lines.append(_table_line([width], [""]))
|
|
184
|
+
|
|
185
|
+
lines.append(f"{bl}{h * (width - 2)}{br}")
|
|
186
|
+
return "\n".join(lines)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def format_json(data: Any) -> str:
|
|
190
|
+
return json.dumps(data, indent=2, default=str, ensure_ascii=False)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def format_summary(result: dict[str, Any], faults: list[dict[str, Any]]) -> str:
|
|
194
|
+
"""Human-readable paragraph summary."""
|
|
195
|
+
state = result.get("state", "unknown")
|
|
196
|
+
icon = _status_icon(state)
|
|
197
|
+
lines = [
|
|
198
|
+
f"{icon} CFA: {state.upper()}",
|
|
199
|
+
f"Intent: {result.get('intent', '')}",
|
|
200
|
+
f"Hash: {result.get('signature_hash', 'n/a')}",
|
|
201
|
+
f"Policy: {result.get('policy_bundle', '')}",
|
|
202
|
+
f"Replans: {result.get('replan_count', 0)}",
|
|
203
|
+
]
|
|
204
|
+
if faults:
|
|
205
|
+
lines.append(f"Faults: {len(faults)}")
|
|
206
|
+
for f in faults:
|
|
207
|
+
lines.append(f" [{_severity_color(f.get('severity', 'high'))}] {f.get('code', '')}")
|
|
208
|
+
return "\n".join(lines)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def format_rules_table(rules: list[dict[str, str]]) -> str:
|
|
212
|
+
h, tl, tr, bl, br = _box("h"), _box("tl"), _box("tr"), _box("bl"), _box("br")
|
|
213
|
+
width = 80
|
|
214
|
+
lines: list[str] = []
|
|
215
|
+
title = " CFA Policy Rules "
|
|
216
|
+
lines.append(f"{tl}{title.center(width - 2, h)}{tr}")
|
|
217
|
+
|
|
218
|
+
header_w = [28, 12, 20, 8, 8]
|
|
219
|
+
lines.append(_table_line(header_w, ["NAME", "ACTION", "FAULT CODE", "SEVERITY", "FAMILY"]))
|
|
220
|
+
lines.append(_table_sep(header_w))
|
|
221
|
+
for r in rules:
|
|
222
|
+
lines.append(_table_line(header_w, [
|
|
223
|
+
r.get("name", "")[:26],
|
|
224
|
+
r.get("action", "").upper()[:10],
|
|
225
|
+
r.get("fault_code", "")[:18],
|
|
226
|
+
r.get("severity", "")[:6],
|
|
227
|
+
r.get("family", "")[:6],
|
|
228
|
+
]))
|
|
229
|
+
lines.append(f"{bl}{h * (width - 2)}{br}")
|
|
230
|
+
return "\n".join(lines)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def format_backends_list(backends: list[dict[str, Any]]) -> str:
|
|
234
|
+
h, tl, tr, bl, br = _box("h"), _box("tl"), _box("tr"), _box("bl"), _box("br")
|
|
235
|
+
width = 60
|
|
236
|
+
lines: list[str] = []
|
|
237
|
+
title = " CFA Registered Backends "
|
|
238
|
+
lines.append(f"{tl}{title.center(width - 2, h)}{tr}")
|
|
239
|
+
header_w = [20, 18, 18]
|
|
240
|
+
lines.append(_table_line(header_w, ["NAME", "MERGE", "ANONYMIZE"]))
|
|
241
|
+
lines.append(_table_sep(header_w))
|
|
242
|
+
ok, fail = _c(_GREEN, _icon("approved")), _c(_RED, _icon("blocked"))
|
|
243
|
+
for b in backends:
|
|
244
|
+
lines.append(_table_line(header_w, [
|
|
245
|
+
b.get("name", "")[:18],
|
|
246
|
+
ok if b.get("supports_merge") else fail,
|
|
247
|
+
ok if b.get("supports_anonymization") else fail,
|
|
248
|
+
]))
|
|
249
|
+
lines.append(f"{bl}{h * (width - 2)}{br}")
|
|
250
|
+
return "\n".join(lines)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def format_audit_table(events: list[dict[str, Any]], chain_intact: bool = True) -> str:
|
|
254
|
+
h, tl, tr, bl, br = _box("h"), _box("tl"), _box("tr"), _box("bl"), _box("br")
|
|
255
|
+
width = 80
|
|
256
|
+
lines: list[str] = []
|
|
257
|
+
title = " CFA Audit Trail "
|
|
258
|
+
lines.append(f"{tl}{title.center(width - 2, h)}{tr}")
|
|
259
|
+
|
|
260
|
+
chain_status = _c(_GREEN, f"{_icon('approved')} INTACT") if chain_intact else _c(_RED, f"{_icon('blocked')} BROKEN")
|
|
261
|
+
lines.append(_table_line([width], [f"Chain: {chain_status} | Events: {len(events)}"]))
|
|
262
|
+
lines.append(_table_line([width], [""]))
|
|
263
|
+
|
|
264
|
+
if events:
|
|
265
|
+
header_w = [4, 21, 16, 14, 19]
|
|
266
|
+
lines.append(_table_line(header_w, ["#", "TIMESTAMP", "PHASE", "EVENT", "OUTCOME"]))
|
|
267
|
+
lines.append(_table_sep(header_w))
|
|
268
|
+
for i, e in enumerate(events[:50]):
|
|
269
|
+
ts = e.get("timestamp", "")[:19]
|
|
270
|
+
lines.append(_table_line(header_w, [
|
|
271
|
+
str(i + 1),
|
|
272
|
+
ts,
|
|
273
|
+
e.get("phase", e.get("stage", ""))[:14],
|
|
274
|
+
e.get("event_type", "")[:12],
|
|
275
|
+
e.get("outcome", "")[:17],
|
|
276
|
+
]))
|
|
277
|
+
if len(events) > 50:
|
|
278
|
+
lines.append(_table_line([width], [f" ... and {len(events) - 50} more events"]))
|
|
279
|
+
lines.append(f"{bl}{h * (width - 2)}{br}")
|
|
280
|
+
return "\n".join(lines)
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""cfa audit — audit trail operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cmd_audit_show(args) -> int:
|
|
10
|
+
from cfa.audit.trail import AuditTrail, JsonLinesAuditStorage
|
|
11
|
+
from cfa.cli.formatters import format_audit_table, format_json
|
|
12
|
+
|
|
13
|
+
audit_path = args.file or args.data_dir
|
|
14
|
+
storage = JsonLinesAuditStorage(audit_path) if audit_path else None
|
|
15
|
+
trail = AuditTrail(storage=storage)
|
|
16
|
+
events = trail.get_events_for_intent(args.id)
|
|
17
|
+
event_dicts = [
|
|
18
|
+
{
|
|
19
|
+
"timestamp": e.timestamp.isoformat() if hasattr(e.timestamp, "isoformat") else str(e.timestamp),
|
|
20
|
+
"phase": e.stage,
|
|
21
|
+
"event_type": e.event_type,
|
|
22
|
+
"outcome": e.outcome,
|
|
23
|
+
"intent_id": e.intent_id,
|
|
24
|
+
}
|
|
25
|
+
for e in events
|
|
26
|
+
]
|
|
27
|
+
chain_ok = trail.verify_chain()
|
|
28
|
+
|
|
29
|
+
fmt = args.format or "table"
|
|
30
|
+
if fmt == "json":
|
|
31
|
+
print(format_json({"intent_id": args.id, "chain_intact": chain_ok, "events": event_dicts}))
|
|
32
|
+
else:
|
|
33
|
+
print(format_audit_table(event_dicts, chain_ok))
|
|
34
|
+
|
|
35
|
+
if args.output:
|
|
36
|
+
out = Path(args.output)
|
|
37
|
+
out.write_text(format_json({"intent_id": args.id, "chain_intact": chain_ok, "events": event_dicts}), encoding="utf-8")
|
|
38
|
+
print(f"Saved to {out}")
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cmd_audit_verify(args) -> int:
|
|
43
|
+
from cfa.audit.trail import AuditTrail, JsonLinesAuditStorage
|
|
44
|
+
|
|
45
|
+
audit_path = args.file or args.data_dir
|
|
46
|
+
storage = JsonLinesAuditStorage(audit_path) if audit_path else None
|
|
47
|
+
trail = AuditTrail(storage=storage)
|
|
48
|
+
|
|
49
|
+
if args.id:
|
|
50
|
+
events = trail.get_events_for_intent(args.id)
|
|
51
|
+
chain_ok = trail.verify_chain()
|
|
52
|
+
if chain_ok:
|
|
53
|
+
print(f"\u2713 Chain INTACT for intent {args.id} ({len(events)} events)")
|
|
54
|
+
return 0
|
|
55
|
+
else:
|
|
56
|
+
print(f"\u2717 Chain BROKEN for intent {args.id}", file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
else:
|
|
59
|
+
all_ok = trail.verify_chain()
|
|
60
|
+
if all_ok:
|
|
61
|
+
print(f"\u2713 Chain INTACT ({trail.event_count} events total)")
|
|
62
|
+
return 0
|
|
63
|
+
else:
|
|
64
|
+
print("\u2717 Chain BROKEN", file=sys.stderr)
|
|
65
|
+
return 1
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""cfa catalog — validate catalog files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .._helpers import load_catalog
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cmd_catalog_validate(args) -> int:
|
|
9
|
+
from cfa.cli.formatters import format_json
|
|
10
|
+
from cfa.policy.catalog import validate_catalog
|
|
11
|
+
|
|
12
|
+
catalog = load_catalog(args.path)
|
|
13
|
+
result = validate_catalog(catalog, require_datasets=args.require_datasets)
|
|
14
|
+
output = {
|
|
15
|
+
"path": args.path,
|
|
16
|
+
"valid": result.valid,
|
|
17
|
+
"issue_count": len(result.issues),
|
|
18
|
+
"issues": [{"path": i.path, "message": i.message} for i in result.issues],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if args.format == "json":
|
|
22
|
+
print(format_json(output))
|
|
23
|
+
else:
|
|
24
|
+
status = "VALID" if result.valid else "INVALID"
|
|
25
|
+
print(f"Catalog {status}: {args.path}")
|
|
26
|
+
for msg in result.messages:
|
|
27
|
+
print(f" - {msg}")
|
|
28
|
+
return 0 if result.valid else 1
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""cfa policy — evaluate and validate policy bundles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .._helpers import load_catalog, load_policy, load_structured_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _cross_validate_signature_against_catalog(signature, catalog: dict) -> list[dict[str, str]]:
|
|
12
|
+
errors: list[dict[str, str]] = []
|
|
13
|
+
catalog_datasets = catalog.get("datasets", {})
|
|
14
|
+
if not isinstance(catalog_datasets, dict):
|
|
15
|
+
errors.append({"path": "catalog.datasets", "message": "must be an object keyed by dataset name"})
|
|
16
|
+
return errors
|
|
17
|
+
for idx, ds in enumerate(signature.datasets):
|
|
18
|
+
base = f"signature.datasets[{idx}]"
|
|
19
|
+
cat_entry = catalog_datasets.get(ds.name)
|
|
20
|
+
if cat_entry is None:
|
|
21
|
+
errors.append({"path": f"{base}.name", "message": f"dataset '{ds.name}' not found in catalog"})
|
|
22
|
+
continue
|
|
23
|
+
if not isinstance(cat_entry, dict):
|
|
24
|
+
errors.append({"path": f"{base}.name", "message": f"catalog entry for '{ds.name}' is not an object"})
|
|
25
|
+
continue
|
|
26
|
+
cat_classification = cat_entry.get("classification", "internal")
|
|
27
|
+
if ds.classification.value != cat_classification:
|
|
28
|
+
errors.append({"path": f"{base}.classification", "message": f"signature says '{ds.classification.value}' but catalog says '{cat_classification}'"})
|
|
29
|
+
cat_pii = set(cat_entry.get("pii_columns", []))
|
|
30
|
+
sig_pii = set(ds.pii_columns)
|
|
31
|
+
if cat_pii - sig_pii:
|
|
32
|
+
errors.append({"path": f"{base}.pii_columns", "message": f"signature missing PII columns from catalog: {sorted(cat_pii - sig_pii)}"})
|
|
33
|
+
if sig_pii - cat_pii:
|
|
34
|
+
errors.append({"path": f"{base}.pii_columns", "message": f"signature declares PII columns not in catalog: {sorted(sig_pii - cat_pii)}"})
|
|
35
|
+
cat_partition = cat_entry.get("partition_column")
|
|
36
|
+
if cat_partition and ds.partition_column and cat_partition != ds.partition_column:
|
|
37
|
+
errors.append({"path": f"{base}.partition_column", "message": f"signature says '{ds.partition_column}' but catalog says '{cat_partition}'"})
|
|
38
|
+
return errors
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_policy_check(args) -> int:
|
|
42
|
+
from cfa.audit.hashing import hash_file_content, hash_governance_artifact
|
|
43
|
+
from cfa.audit.trail import AuditTrail, JsonLinesAuditStorage
|
|
44
|
+
from cfa.cli.formatters import format_json
|
|
45
|
+
from cfa.policy.engine import PolicyEngine
|
|
46
|
+
from cfa.types import PolicyAction, StateSignature
|
|
47
|
+
from cfa.validation.signature import unwrap_signature_data, validate_signature_data
|
|
48
|
+
|
|
49
|
+
data = load_structured_file(args.signature, "Error: PyYAML required for YAML signatures.")
|
|
50
|
+
validation = validate_signature_data(data, require_datasets=args.require_datasets)
|
|
51
|
+
if not validation.valid:
|
|
52
|
+
output = {"signature": args.signature, "valid": False, "issue_count": len(validation.issues), "issues": [{"path": i.path, "message": i.message} for i in validation.issues]}
|
|
53
|
+
if args.format == "json": print(format_json(output))
|
|
54
|
+
else:
|
|
55
|
+
print(f"StateSignature INVALID: {args.signature}")
|
|
56
|
+
for msg in validation.messages: print(f" - {msg}")
|
|
57
|
+
return 1
|
|
58
|
+
|
|
59
|
+
signature_data = unwrap_signature_data(data)
|
|
60
|
+
signature = StateSignature.from_dict(signature_data)
|
|
61
|
+
catalog = load_catalog(args.catalog)
|
|
62
|
+
|
|
63
|
+
if args.strict and catalog:
|
|
64
|
+
strict_errors = _cross_validate_signature_against_catalog(signature, catalog)
|
|
65
|
+
if strict_errors:
|
|
66
|
+
output = {"signature": args.signature, "valid": False, "issue_count": len(strict_errors), "issues": strict_errors}
|
|
67
|
+
if args.format == "json": print(format_json(output))
|
|
68
|
+
else:
|
|
69
|
+
print(f"StateSignature/catalog mismatch: {args.signature}")
|
|
70
|
+
for issue in strict_errors: print(f" - {issue['path']}: {issue['message']}")
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
policy_rules, bundle_version = load_policy(args.policy_bundle)
|
|
74
|
+
engine = PolicyEngine(rules=policy_rules, policy_bundle_version=bundle_version)
|
|
75
|
+
result = engine.evaluate(signature)
|
|
76
|
+
decision_id = str(uuid.uuid4())
|
|
77
|
+
|
|
78
|
+
catalog_hash = hash_governance_artifact(catalog) if catalog else ""
|
|
79
|
+
policy_bundle_hash = ""
|
|
80
|
+
if Path(args.policy_bundle).suffix in (".yaml", ".yml", ".json"):
|
|
81
|
+
policy_bundle_hash = hash_file_content(args.policy_bundle)
|
|
82
|
+
|
|
83
|
+
faults = [{"code": f.code, "severity": f.severity.value, "family": f.family.value, "message": f.message, "remediation": list(f.remediation)} for f in result.faults]
|
|
84
|
+
audit_event_hash = ""
|
|
85
|
+
if args.audit_log:
|
|
86
|
+
audit = AuditTrail(storage=JsonLinesAuditStorage(args.audit_log))
|
|
87
|
+
event = audit.record(intent_id=signature.intent_id, stage="policy_check", event_type="policy_evaluation", outcome=result.action.value, policy_bundle_version=engine.policy_bundle_version, decision_id=decision_id, signature_hash=signature.signature_hash, catalog_hash=catalog_hash, policy_bundle_hash=policy_bundle_hash, faults=[f["code"] for f in faults], interventions=result.interventions, reasoning=result.reasoning)
|
|
88
|
+
audit_event_hash = event.event_hash
|
|
89
|
+
|
|
90
|
+
output = {"schema_version": "cfa.policy_check.v1", "decision_id": decision_id, "signature_hash": signature.signature_hash, "policy_bundle": engine.policy_bundle_version, "catalog_hash": catalog_hash, "policy_bundle_hash": policy_bundle_hash, "action": result.action.value, "passed": result.action == PolicyAction.APPROVE, "faults": faults, "interventions": result.interventions, "reasoning": result.reasoning, "audit_event_hash": audit_event_hash}
|
|
91
|
+
|
|
92
|
+
if args.format == "json":
|
|
93
|
+
print(format_json(output))
|
|
94
|
+
else:
|
|
95
|
+
print(f"Policy check {result.action.value.upper()}: {args.signature}")
|
|
96
|
+
print(f" decision_id: {decision_id}")
|
|
97
|
+
print(f" signature_hash: {signature.signature_hash}")
|
|
98
|
+
print(f" policy_bundle: {engine.policy_bundle_version}")
|
|
99
|
+
if catalog_hash: print(f" catalog_hash: {catalog_hash}")
|
|
100
|
+
if policy_bundle_hash: print(f" policy_bundle_hash: {policy_bundle_hash}")
|
|
101
|
+
if audit_event_hash: print(f" audit_hash: {audit_event_hash}")
|
|
102
|
+
if result.reasoning: print(f" reasoning: {result.reasoning}")
|
|
103
|
+
for fault in faults: print(f" - [{fault['severity']}] {fault['code']}: {fault['message']}")
|
|
104
|
+
return 1 if (args.exit_code and result.action != PolicyAction.APPROVE) else 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_policy_validate(args) -> int:
|
|
108
|
+
from cfa.cli.formatters import format_json
|
|
109
|
+
from cfa.policy.bundle import validate_policy_bundle_data
|
|
110
|
+
|
|
111
|
+
data = load_structured_file(args.path, "Error: PyYAML required for YAML policy bundles.")
|
|
112
|
+
result = validate_policy_bundle_data(data)
|
|
113
|
+
output = {"path": args.path, "valid": result.valid, "issue_count": len(result.issues), "issues": [{"path": i.path, "message": i.message} for i in result.issues]}
|
|
114
|
+
if args.format == "json": print(format_json(output))
|
|
115
|
+
else:
|
|
116
|
+
status = "VALID" if result.valid else "INVALID"
|
|
117
|
+
print(f"Policy bundle {status}: {args.path}")
|
|
118
|
+
for msg in result.messages: print(f" - {msg}")
|
|
119
|
+
return 0 if result.valid else 1
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""cfa rules — policy rule operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cmd_rules_list(args) -> int:
|
|
9
|
+
from cfa.cli.formatters import format_json, format_rules_table
|
|
10
|
+
from cfa.policy.engine import PolicyEngine
|
|
11
|
+
|
|
12
|
+
engine = PolicyEngine(policy_bundle_version=args.policy_bundle)
|
|
13
|
+
rule_dicts = engine.describe_rules()
|
|
14
|
+
fmt = args.format or "table"
|
|
15
|
+
|
|
16
|
+
if fmt == "json":
|
|
17
|
+
print(format_json(rule_dicts))
|
|
18
|
+
else:
|
|
19
|
+
print(format_rules_table(rule_dicts))
|
|
20
|
+
print(f"\nPolicy bundle: {args.policy_bundle} | Rules: {len(rule_dicts)}")
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def cmd_rules_explain(args) -> int:
|
|
25
|
+
from cfa.policy.engine import PolicyEngine
|
|
26
|
+
|
|
27
|
+
engine = PolicyEngine(policy_bundle_version=args.policy_bundle)
|
|
28
|
+
for r in engine.rules:
|
|
29
|
+
if r.fault_code == args.code:
|
|
30
|
+
print(f"Fault Code: {r.fault_code}")
|
|
31
|
+
print(f"Rule: {r.name}")
|
|
32
|
+
print(f"Action: {r.action.value.upper()}")
|
|
33
|
+
print(f"Severity: {r.severity.value.upper()}")
|
|
34
|
+
print(f"Family: {r.fault_family.value}")
|
|
35
|
+
print(f"Message: {r.message}")
|
|
36
|
+
if r.remediation:
|
|
37
|
+
print("Remediation:")
|
|
38
|
+
for i, rem in enumerate(r.remediation):
|
|
39
|
+
print(f" {i+1}. {rem}")
|
|
40
|
+
return 0
|
|
41
|
+
print(f"Unknown fault code: {args.code}", file=sys.stderr)
|
|
42
|
+
return 1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""cfa signature — validate StateSignature files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .._helpers import load_structured_file
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cmd_signature_validate(args) -> int:
|
|
9
|
+
from cfa.cli.formatters import format_json
|
|
10
|
+
from cfa.validation.signature import validate_signature_data
|
|
11
|
+
|
|
12
|
+
data = load_structured_file(
|
|
13
|
+
args.path,
|
|
14
|
+
"Error: PyYAML required for YAML signatures. Install: pip install pyyaml",
|
|
15
|
+
)
|
|
16
|
+
result = validate_signature_data(data, require_datasets=args.require_datasets)
|
|
17
|
+
output = {
|
|
18
|
+
"path": args.path,
|
|
19
|
+
"valid": result.valid,
|
|
20
|
+
"issue_count": len(result.issues),
|
|
21
|
+
"issues": [{"path": i.path, "message": i.message} for i in result.issues],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if args.format == "json":
|
|
25
|
+
print(format_json(output))
|
|
26
|
+
else:
|
|
27
|
+
status = "VALID" if result.valid else "INVALID"
|
|
28
|
+
print(f"StateSignature {status}: {args.path}")
|
|
29
|
+
for msg in result.messages:
|
|
30
|
+
print(f" - {msg}")
|
|
31
|
+
return 0 if result.valid else 1
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""cfa backend list — list registered backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cmd_backend_list(args) -> int:
|
|
9
|
+
from cfa.backends import BackendRegistry
|
|
10
|
+
from cfa.cli.formatters import format_backends_list, format_json
|
|
11
|
+
|
|
12
|
+
registry = BackendRegistry.singleton()
|
|
13
|
+
names = registry.list()
|
|
14
|
+
backends: list[dict[str, Any]] = []
|
|
15
|
+
for name in names:
|
|
16
|
+
factory = registry.get(name)
|
|
17
|
+
backend = factory()
|
|
18
|
+
caps = backend.get_capabilities() if hasattr(backend, "get_capabilities") else None
|
|
19
|
+
backends.append({"name": name, "supports_merge": caps.supports_merge if caps else False, "supports_anonymization": caps.supports_anonymization if caps else False, "supports_partition_overwrite": caps.supports_partition_overwrite if caps else False, "cost_model_available": caps.cost_model_available if caps else False})
|
|
20
|
+
|
|
21
|
+
fmt = args.format or "table"
|
|
22
|
+
if fmt == "json": print(format_json(backends))
|
|
23
|
+
else: print(format_backends_list(backends))
|
|
24
|
+
return 0
|