spanforge 1.0.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.
Files changed (174) hide show
  1. spanforge/__init__.py +815 -0
  2. spanforge/_ansi.py +93 -0
  3. spanforge/_batch_exporter.py +409 -0
  4. spanforge/_cli.py +2094 -0
  5. spanforge/_cli_audit.py +639 -0
  6. spanforge/_cli_compliance.py +711 -0
  7. spanforge/_cli_cost.py +243 -0
  8. spanforge/_cli_ops.py +791 -0
  9. spanforge/_cli_phase11.py +356 -0
  10. spanforge/_hooks.py +337 -0
  11. spanforge/_server.py +1708 -0
  12. spanforge/_span.py +1036 -0
  13. spanforge/_store.py +288 -0
  14. spanforge/_stream.py +664 -0
  15. spanforge/_trace.py +335 -0
  16. spanforge/_tracer.py +254 -0
  17. spanforge/actor.py +141 -0
  18. spanforge/alerts.py +469 -0
  19. spanforge/auto.py +464 -0
  20. spanforge/baseline.py +335 -0
  21. spanforge/cache.py +635 -0
  22. spanforge/compliance.py +325 -0
  23. spanforge/config.py +532 -0
  24. spanforge/consent.py +228 -0
  25. spanforge/consumer.py +377 -0
  26. spanforge/core/__init__.py +5 -0
  27. spanforge/core/compliance_mapping.py +1254 -0
  28. spanforge/cost.py +600 -0
  29. spanforge/debug.py +548 -0
  30. spanforge/deprecations.py +205 -0
  31. spanforge/drift.py +482 -0
  32. spanforge/egress.py +58 -0
  33. spanforge/eval.py +648 -0
  34. spanforge/event.py +1064 -0
  35. spanforge/exceptions.py +240 -0
  36. spanforge/explain.py +178 -0
  37. spanforge/export/__init__.py +69 -0
  38. spanforge/export/append_only.py +337 -0
  39. spanforge/export/cloud.py +357 -0
  40. spanforge/export/datadog.py +497 -0
  41. spanforge/export/grafana.py +320 -0
  42. spanforge/export/jsonl.py +195 -0
  43. spanforge/export/openinference.py +158 -0
  44. spanforge/export/otel_bridge.py +294 -0
  45. spanforge/export/otlp.py +811 -0
  46. spanforge/export/otlp_bridge.py +233 -0
  47. spanforge/export/redis_backend.py +282 -0
  48. spanforge/export/siem_schema.py +98 -0
  49. spanforge/export/siem_splunk.py +264 -0
  50. spanforge/export/siem_syslog.py +212 -0
  51. spanforge/export/webhook.py +299 -0
  52. spanforge/exporters/__init__.py +30 -0
  53. spanforge/exporters/console.py +271 -0
  54. spanforge/exporters/jsonl.py +144 -0
  55. spanforge/exporters/sqlite.py +142 -0
  56. spanforge/gate.py +1150 -0
  57. spanforge/governance.py +181 -0
  58. spanforge/hitl.py +295 -0
  59. spanforge/http.py +187 -0
  60. spanforge/inspect.py +427 -0
  61. spanforge/integrations/__init__.py +45 -0
  62. spanforge/integrations/_pricing.py +280 -0
  63. spanforge/integrations/anthropic.py +388 -0
  64. spanforge/integrations/azure_openai.py +133 -0
  65. spanforge/integrations/bedrock.py +292 -0
  66. spanforge/integrations/crewai.py +251 -0
  67. spanforge/integrations/gemini.py +351 -0
  68. spanforge/integrations/groq.py +442 -0
  69. spanforge/integrations/langchain.py +349 -0
  70. spanforge/integrations/langgraph.py +306 -0
  71. spanforge/integrations/llamaindex.py +373 -0
  72. spanforge/integrations/ollama.py +287 -0
  73. spanforge/integrations/openai.py +368 -0
  74. spanforge/integrations/together.py +483 -0
  75. spanforge/io.py +214 -0
  76. spanforge/lint.py +322 -0
  77. spanforge/metrics.py +417 -0
  78. spanforge/metrics_export.py +343 -0
  79. spanforge/migrate.py +402 -0
  80. spanforge/model_registry.py +278 -0
  81. spanforge/models.py +389 -0
  82. spanforge/namespaces/__init__.py +254 -0
  83. spanforge/namespaces/audit.py +256 -0
  84. spanforge/namespaces/cache.py +237 -0
  85. spanforge/namespaces/chain.py +77 -0
  86. spanforge/namespaces/confidence.py +72 -0
  87. spanforge/namespaces/consent.py +92 -0
  88. spanforge/namespaces/cost.py +179 -0
  89. spanforge/namespaces/decision.py +143 -0
  90. spanforge/namespaces/diff.py +157 -0
  91. spanforge/namespaces/drift.py +80 -0
  92. spanforge/namespaces/eval_.py +251 -0
  93. spanforge/namespaces/feedback.py +241 -0
  94. spanforge/namespaces/fence.py +193 -0
  95. spanforge/namespaces/guard.py +105 -0
  96. spanforge/namespaces/hitl.py +91 -0
  97. spanforge/namespaces/latency.py +72 -0
  98. spanforge/namespaces/prompt.py +190 -0
  99. spanforge/namespaces/redact.py +173 -0
  100. spanforge/namespaces/retrieval.py +379 -0
  101. spanforge/namespaces/runtime_governance.py +494 -0
  102. spanforge/namespaces/template.py +208 -0
  103. spanforge/namespaces/tool_call.py +77 -0
  104. spanforge/namespaces/trace.py +1029 -0
  105. spanforge/normalizer.py +171 -0
  106. spanforge/plugins.py +82 -0
  107. spanforge/presidio_backend.py +349 -0
  108. spanforge/processor.py +258 -0
  109. spanforge/prompt_registry.py +418 -0
  110. spanforge/py.typed +0 -0
  111. spanforge/redact.py +914 -0
  112. spanforge/regression.py +192 -0
  113. spanforge/runtime_policy.py +159 -0
  114. spanforge/sampling.py +511 -0
  115. spanforge/schema.py +183 -0
  116. spanforge/schemas/v1.0/schema.json +170 -0
  117. spanforge/schemas/v2.0/schema.json +536 -0
  118. spanforge/sdk/__init__.py +625 -0
  119. spanforge/sdk/_base.py +584 -0
  120. spanforge/sdk/_base.pyi +71 -0
  121. spanforge/sdk/_exceptions.py +1096 -0
  122. spanforge/sdk/_types.py +2184 -0
  123. spanforge/sdk/alert.py +1514 -0
  124. spanforge/sdk/alert.pyi +56 -0
  125. spanforge/sdk/audit.py +1196 -0
  126. spanforge/sdk/audit.pyi +67 -0
  127. spanforge/sdk/cec.py +1215 -0
  128. spanforge/sdk/cec.pyi +37 -0
  129. spanforge/sdk/config.py +641 -0
  130. spanforge/sdk/config.pyi +55 -0
  131. spanforge/sdk/enterprise.py +714 -0
  132. spanforge/sdk/enterprise.pyi +79 -0
  133. spanforge/sdk/explain.py +170 -0
  134. spanforge/sdk/fallback.py +432 -0
  135. spanforge/sdk/feedback.py +351 -0
  136. spanforge/sdk/gate.py +874 -0
  137. spanforge/sdk/gate.pyi +51 -0
  138. spanforge/sdk/identity.py +2114 -0
  139. spanforge/sdk/identity.pyi +47 -0
  140. spanforge/sdk/lineage.py +175 -0
  141. spanforge/sdk/observe.py +1065 -0
  142. spanforge/sdk/observe.pyi +50 -0
  143. spanforge/sdk/operator.py +338 -0
  144. spanforge/sdk/pii.py +1473 -0
  145. spanforge/sdk/pii.pyi +119 -0
  146. spanforge/sdk/pipelines.py +458 -0
  147. spanforge/sdk/pipelines.pyi +39 -0
  148. spanforge/sdk/policy.py +930 -0
  149. spanforge/sdk/rag.py +594 -0
  150. spanforge/sdk/rbac.py +280 -0
  151. spanforge/sdk/registry.py +430 -0
  152. spanforge/sdk/registry.pyi +46 -0
  153. spanforge/sdk/scope.py +279 -0
  154. spanforge/sdk/secrets.py +293 -0
  155. spanforge/sdk/secrets.pyi +25 -0
  156. spanforge/sdk/security.py +560 -0
  157. spanforge/sdk/security.pyi +57 -0
  158. spanforge/sdk/trust.py +472 -0
  159. spanforge/sdk/trust.pyi +41 -0
  160. spanforge/secrets.py +799 -0
  161. spanforge/signing.py +1179 -0
  162. spanforge/stats.py +100 -0
  163. spanforge/stream.py +560 -0
  164. spanforge/testing.py +378 -0
  165. spanforge/testing_mocks.py +1052 -0
  166. spanforge/trace.py +199 -0
  167. spanforge/types.py +696 -0
  168. spanforge/ulid.py +300 -0
  169. spanforge/validate.py +379 -0
  170. spanforge-1.0.0.dist-info/METADATA +1509 -0
  171. spanforge-1.0.0.dist-info/RECORD +174 -0
  172. spanforge-1.0.0.dist-info/WHEEL +4 -0
  173. spanforge-1.0.0.dist-info/entry_points.txt +5 -0
  174. spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
spanforge/_cli_ops.py ADDED
@@ -0,0 +1,791 @@
1
+ """Operational command groups for the SpanForge CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any, cast
11
+
12
+
13
+ def add_ops_subcommands(
14
+ sub: argparse._SubParsersAction[argparse.ArgumentParser],
15
+ ) -> tuple[
16
+ argparse.ArgumentParser,
17
+ argparse.ArgumentParser,
18
+ argparse.ArgumentParser,
19
+ argparse.ArgumentParser,
20
+ ]:
21
+ """Register operational CLI subcommands."""
22
+ config_parser = sub.add_parser(
23
+ "config",
24
+ help="Integration config management (.halluccheck.toml)",
25
+ )
26
+ config_sub = config_parser.add_subparsers(dest="config_command", metavar="<action>")
27
+
28
+ config_validate_parser = config_sub.add_parser(
29
+ "validate",
30
+ help="Validate a .halluccheck.toml config file against the v6.0 schema",
31
+ )
32
+ config_validate_parser.add_argument(
33
+ "--file",
34
+ default=None,
35
+ metavar="PATH",
36
+ help="Path to .halluccheck.toml (default: auto-discover in cwd or ~)",
37
+ )
38
+
39
+ trust_parser = sub.add_parser(
40
+ "trust",
41
+ help="T.R.U.S.T. scorecard operations",
42
+ )
43
+ trust_sub = trust_parser.add_subparsers(dest="trust_command", metavar="<action>")
44
+
45
+ trust_scorecard_parser = trust_sub.add_parser(
46
+ "scorecard",
47
+ help="Show T.R.U.S.T. scorecard for a project",
48
+ )
49
+ trust_scorecard_parser.add_argument(
50
+ "--project-id",
51
+ default="",
52
+ metavar="ID",
53
+ help="Project ID to query (default: from config)",
54
+ )
55
+ trust_scorecard_parser.add_argument(
56
+ "--format",
57
+ choices=["text", "json"],
58
+ default="text",
59
+ help="Output format (default: text)",
60
+ )
61
+
62
+ trust_badge_parser = trust_sub.add_parser(
63
+ "badge",
64
+ help="Generate T.R.U.S.T. badge SVG for a project",
65
+ )
66
+ trust_badge_parser.add_argument(
67
+ "--project-id",
68
+ default="",
69
+ metavar="ID",
70
+ help="Project ID (default: from config)",
71
+ )
72
+ trust_badge_parser.add_argument(
73
+ "--output",
74
+ default=None,
75
+ metavar="PATH",
76
+ help="Write SVG to file instead of stdout",
77
+ )
78
+
79
+ trust_gate_parser = trust_sub.add_parser(
80
+ "gate",
81
+ help="Run composite trust gate (TRS-020)",
82
+ )
83
+ trust_gate_parser.add_argument(
84
+ "--project-id",
85
+ default="",
86
+ metavar="ID",
87
+ help="Project ID (default: from config)",
88
+ )
89
+ trust_gate_parser.add_argument(
90
+ "--min-score",
91
+ type=float,
92
+ default=60.0,
93
+ metavar="N",
94
+ help="Minimum T.R.U.S.T. score to pass (default: 60)",
95
+ )
96
+ trust_gate_parser.add_argument(
97
+ "--format",
98
+ choices=["text", "json"],
99
+ default="text",
100
+ help="Output format (default: text)",
101
+ )
102
+
103
+ gate_parser = sub.add_parser(
104
+ "gate",
105
+ help="CI/CD gate pipeline commands (evaluate, run, trust-gate)",
106
+ )
107
+ gate_sub = gate_parser.add_subparsers(dest="gate_command", metavar="<action>")
108
+
109
+ gate_run_parser = gate_sub.add_parser(
110
+ "run",
111
+ help="Parse and execute a YAML gate pipeline file",
112
+ )
113
+ gate_run_parser.add_argument(
114
+ "gate_yaml",
115
+ metavar="GATE_YAML",
116
+ help="Path to the gate pipeline YAML file",
117
+ )
118
+ gate_run_parser.add_argument(
119
+ "--context",
120
+ action="append",
121
+ metavar="KEY=VALUE",
122
+ help="Context variable for ${var} substitution (repeatable)",
123
+ )
124
+ gate_run_parser.add_argument(
125
+ "--artifact-dir",
126
+ dest="artifact_dir",
127
+ default=None,
128
+ metavar="DIR",
129
+ help="Override the artifact storage directory",
130
+ )
131
+ gate_run_parser.add_argument(
132
+ "--format",
133
+ choices=["text", "json"],
134
+ default="text",
135
+ help="Output format (default: text)",
136
+ )
137
+
138
+ gate_eval_parser = gate_sub.add_parser(
139
+ "evaluate",
140
+ help="Evaluate a single named gate against a payload file or stdin",
141
+ )
142
+ gate_eval_parser.add_argument(
143
+ "gate_id",
144
+ metavar="GATE_ID",
145
+ help="Gate identifier to evaluate",
146
+ )
147
+ gate_eval_parser.add_argument(
148
+ "--payload",
149
+ default=None,
150
+ metavar="FILE",
151
+ help="Path to a JSON payload file (default: read from stdin)",
152
+ )
153
+ gate_eval_parser.add_argument(
154
+ "--project-id",
155
+ dest="project_id",
156
+ default="",
157
+ metavar="ID",
158
+ help="Project scope for artifact isolation",
159
+ )
160
+ gate_eval_parser.add_argument(
161
+ "--format",
162
+ choices=["text", "json"],
163
+ default="text",
164
+ help="Output format (default: text)",
165
+ )
166
+
167
+ gate_tg_parser = gate_sub.add_parser(
168
+ "trust-gate",
169
+ help="Run the composite trust gate check against live telemetry windows",
170
+ )
171
+ gate_tg_parser.add_argument(
172
+ "--project-id",
173
+ dest="project_id",
174
+ default="",
175
+ metavar="ID",
176
+ help="Project scope for the trust gate check",
177
+ )
178
+ gate_tg_parser.add_argument(
179
+ "--format",
180
+ choices=["text", "json"],
181
+ default="text",
182
+ help="Output format (default: text)",
183
+ )
184
+
185
+ sub.add_parser(
186
+ "doctor",
187
+ help="Run environment health checks: config, services, patterns, connectivity",
188
+ )
189
+
190
+ operator_parser = sub.add_parser(
191
+ "operator",
192
+ help="Operator workflow inspection and evidence export",
193
+ )
194
+ operator_sub = operator_parser.add_subparsers(dest="operator_command", metavar="<action>")
195
+
196
+ operator_inspect_parser = operator_sub.add_parser(
197
+ "inspect",
198
+ help="Inspect one runtime-governance trace workflow",
199
+ )
200
+ operator_inspect_parser.add_argument(
201
+ "trace_id",
202
+ metavar="TRACE_ID",
203
+ help="Trace identifier to inspect",
204
+ )
205
+ operator_inspect_parser.add_argument(
206
+ "--format",
207
+ choices=["text", "json"],
208
+ default="text",
209
+ help="Output format (default: text)",
210
+ )
211
+
212
+ operator_export_parser = operator_sub.add_parser(
213
+ "export",
214
+ help="Export a signed operator evidence package for one trace",
215
+ )
216
+ operator_export_parser.add_argument(
217
+ "trace_id",
218
+ metavar="TRACE_ID",
219
+ help="Trace identifier to export",
220
+ )
221
+ operator_export_parser.add_argument(
222
+ "--output",
223
+ default=None,
224
+ metavar="PATH",
225
+ help="Optional JSON output path for the export package",
226
+ )
227
+ operator_export_parser.add_argument(
228
+ "--format",
229
+ choices=["text", "json"],
230
+ default="text",
231
+ help="Output format when writing to stdout (default: text)",
232
+ )
233
+
234
+ return config_parser, trust_parser, gate_parser, operator_parser
235
+
236
+
237
+ def dispatch_ops_command(
238
+ args: argparse.Namespace,
239
+ config_parser: argparse.ArgumentParser,
240
+ trust_parser: argparse.ArgumentParser,
241
+ gate_parser: argparse.ArgumentParser,
242
+ operator_parser: argparse.ArgumentParser,
243
+ ) -> int | None:
244
+ """Dispatch operational commands when selected."""
245
+ command = getattr(args, "command", None)
246
+ if command == "config":
247
+ config_action = getattr(args, "config_command", None)
248
+ if config_action == "validate":
249
+ return _cmd_config_validate(args)
250
+ config_parser.print_help()
251
+ return 2
252
+ if command == "trust":
253
+ return _cmd_trust(args, trust_parser)
254
+ if command == "gate":
255
+ return _cmd_gate(args, gate_parser)
256
+ if command == "operator":
257
+ return _cmd_operator(args, operator_parser)
258
+ if command == "doctor":
259
+ return _cmd_doctor(args)
260
+ return None
261
+
262
+
263
+ def _cmd_operator(args: argparse.Namespace, operator_parser: argparse.ArgumentParser) -> int:
264
+ """Handle ``spanforge operator`` subcommands."""
265
+ action = getattr(args, "operator_command", None)
266
+
267
+ if action == "inspect":
268
+ return _cmd_operator_inspect(args)
269
+ if action == "export":
270
+ return _cmd_operator_export(args)
271
+
272
+ operator_parser.print_help()
273
+ return 2
274
+
275
+
276
+ def _cmd_gate(args: argparse.Namespace, gate_parser: argparse.ArgumentParser) -> int:
277
+ """Handle ``spanforge gate`` subcommands."""
278
+ action = getattr(args, "gate_command", None)
279
+
280
+ if action == "run":
281
+ return _cmd_gate_run(args)
282
+ if action == "evaluate":
283
+ return _cmd_gate_evaluate(args)
284
+ if action == "trust-gate":
285
+ return _cmd_trust_gate(args)
286
+
287
+ gate_parser.print_help()
288
+ return 2
289
+
290
+
291
+ def _cmd_gate_run(args: argparse.Namespace) -> int:
292
+ """``spanforge gate run`` - execute a YAML gate pipeline file."""
293
+ import json as _json
294
+
295
+ from spanforge.gate import GateRunner
296
+
297
+ gate_yaml = args.gate_yaml
298
+ fmt = getattr(args, "format", "text")
299
+ artifact_dir = getattr(args, "artifact_dir", None)
300
+ raw_context: list[str] = getattr(args, "context", []) or []
301
+
302
+ context: dict[str, str] = {}
303
+ for kv in raw_context:
304
+ if "=" not in kv:
305
+ print(f"error: --context value must be KEY=VALUE, got {kv!r}", file=sys.stderr)
306
+ return 2
307
+ key, _, value = kv.partition("=")
308
+ context[key.strip()] = value
309
+
310
+ if artifact_dir:
311
+ os.environ.setdefault("SPANFORGE_GATE_ARTIFACT_DIR", artifact_dir)
312
+
313
+ try:
314
+ runner = GateRunner()
315
+ result = runner.run(gate_yaml, context or None)
316
+ except FileNotFoundError:
317
+ print(f"error: gate YAML not found: {gate_yaml}", file=sys.stderr)
318
+ return 2
319
+ except Exception as exc:
320
+ print(f"error: {exc}", file=sys.stderr)
321
+ return 2
322
+
323
+ if fmt == "json":
324
+ print(
325
+ _json.dumps(
326
+ result.to_dict()
327
+ if hasattr(result, "to_dict")
328
+ else {
329
+ "overall_pass": result.overall_pass,
330
+ "exit_code": result.exit_code,
331
+ "run_id": result.run_id,
332
+ "duration_ms": result.duration_ms,
333
+ "gates": [
334
+ {
335
+ "gate_id": gate.gate_id,
336
+ "name": gate.name,
337
+ "verdict": gate.verdict.value
338
+ if hasattr(gate.verdict, "value")
339
+ else str(gate.verdict),
340
+ "duration_ms": gate.duration_ms,
341
+ "detail": gate.detail,
342
+ "metrics": gate.metrics,
343
+ }
344
+ for gate in result.gates
345
+ ],
346
+ },
347
+ indent=2,
348
+ )
349
+ )
350
+ else:
351
+ print(f"Running gate pipeline: {gate_yaml}")
352
+ for gate in result.gates:
353
+ verdict_str = gate.verdict.value if hasattr(gate.verdict, "value") else str(gate.verdict)
354
+ detail_str = f" {gate.detail}" if gate.detail else ""
355
+ print(f" [{verdict_str}] {gate.name or gate.gate_id} ({gate.duration_ms} ms){detail_str}")
356
+ passed = sum(
357
+ 1
358
+ for gate in result.gates
359
+ if str(getattr(gate.verdict, "value", gate.verdict)).upper() in ("PASS",)
360
+ )
361
+ warned = sum(
362
+ 1
363
+ for gate in result.gates
364
+ if str(getattr(gate.verdict, "value", gate.verdict)).upper() in ("WARN",)
365
+ )
366
+ failed = sum(
367
+ 1
368
+ for gate in result.gates
369
+ if str(getattr(gate.verdict, "value", gate.verdict)).upper() in ("FAIL", "ERROR")
370
+ )
371
+ print(f"Result: {passed} passed, {failed} failed, {warned} warned")
372
+
373
+ return int(getattr(result, "exit_code", 0))
374
+
375
+
376
+ def _cmd_gate_evaluate(args: argparse.Namespace) -> int:
377
+ """``spanforge gate evaluate`` - evaluate a single named gate."""
378
+ import json as _json
379
+
380
+ from spanforge.sdk import sf_gate
381
+
382
+ gate_id: str = args.gate_id
383
+ project_id: str = getattr(args, "project_id", "") or ""
384
+ payload_file = getattr(args, "payload", None)
385
+ fmt = getattr(args, "format", "text")
386
+
387
+ payload: dict[str, object] = {}
388
+ if payload_file:
389
+ try:
390
+ with open(payload_file, encoding="utf-8") as fh:
391
+ payload = _json.load(fh)
392
+ except (OSError, _json.JSONDecodeError) as exc:
393
+ print(f"error reading payload: {exc}", file=sys.stderr)
394
+ return 2
395
+ elif not sys.stdin.isatty():
396
+ try:
397
+ payload = _json.load(sys.stdin)
398
+ except _json.JSONDecodeError as exc:
399
+ print(f"error: invalid JSON on stdin: {exc}", file=sys.stderr)
400
+ return 2
401
+
402
+ try:
403
+ result = sf_gate.evaluate(gate_id, payload, project_id=project_id)
404
+ except Exception as exc:
405
+ print(f"error: {exc}", file=sys.stderr)
406
+ return 2
407
+
408
+ verdict_str = result.verdict.value if hasattr(result.verdict, "value") else str(result.verdict)
409
+ exit_code = 0 if verdict_str.upper() in ("PASS", "WARN") else 1
410
+
411
+ if fmt == "json":
412
+ print(
413
+ _json.dumps(
414
+ {
415
+ "gate_id": result.gate_id,
416
+ "verdict": verdict_str,
417
+ "metrics": result.metrics,
418
+ "artifact_url": result.artifact_url,
419
+ "duration_ms": result.duration_ms,
420
+ },
421
+ indent=2,
422
+ )
423
+ )
424
+ else:
425
+ print(f"[{verdict_str}] {gate_id} ({result.duration_ms} ms)")
426
+
427
+ return exit_code
428
+
429
+
430
+ def _cmd_trust(args: argparse.Namespace, trust_parser: argparse.ArgumentParser) -> int:
431
+ """Handle ``spanforge trust`` subcommands."""
432
+ action = getattr(args, "trust_command", None)
433
+
434
+ if action == "scorecard":
435
+ return _cmd_trust_scorecard(args)
436
+ if action == "badge":
437
+ return _cmd_trust_badge(args)
438
+ if action == "gate":
439
+ return _cmd_trust_gate(args)
440
+
441
+ trust_parser.print_help()
442
+ return 2
443
+
444
+
445
+ def _cmd_trust_scorecard(args: argparse.Namespace) -> int:
446
+ """``spanforge trust scorecard`` - show T.R.U.S.T. scorecard."""
447
+ from spanforge.sdk import sf_trust
448
+
449
+ project_id = getattr(args, "project_id", "") or ""
450
+ fmt = getattr(args, "format", "text")
451
+
452
+ try:
453
+ scorecard = sf_trust.get_scorecard(project_id=project_id or None)
454
+ except Exception as exc:
455
+ print(f"error: {exc}", file=sys.stderr)
456
+ return 1
457
+
458
+ if fmt == "json":
459
+ import json as _json
460
+
461
+ data = {
462
+ "project_id": scorecard.project_id,
463
+ "overall_score": scorecard.overall_score,
464
+ "colour_band": scorecard.colour_band,
465
+ "transparency": {
466
+ "score": scorecard.transparency.score,
467
+ "trend": scorecard.transparency.trend,
468
+ },
469
+ "reliability": {
470
+ "score": scorecard.reliability.score,
471
+ "trend": scorecard.reliability.trend,
472
+ },
473
+ "user_trust": {
474
+ "score": scorecard.user_trust.score,
475
+ "trend": scorecard.user_trust.trend,
476
+ },
477
+ "security": {"score": scorecard.security.score, "trend": scorecard.security.trend},
478
+ "traceability": {
479
+ "score": scorecard.traceability.score,
480
+ "trend": scorecard.traceability.trend,
481
+ },
482
+ "record_count": scorecard.record_count,
483
+ }
484
+ print(_json.dumps(data, indent=2))
485
+ else:
486
+ band = scorecard.colour_band.upper()
487
+ print(f"T.R.U.S.T. Scorecard - {scorecard.project_id or '(default project)'}")
488
+ print(f" Overall: {scorecard.overall_score:.1f} [{band}]")
489
+ print(
490
+ f" Transparency: {scorecard.transparency.score:.1f} ({scorecard.transparency.trend})"
491
+ )
492
+ print(f" Reliability: {scorecard.reliability.score:.1f} ({scorecard.reliability.trend})")
493
+ print(f" UserTrust: {scorecard.user_trust.score:.1f} ({scorecard.user_trust.trend})")
494
+ print(f" Security: {scorecard.security.score:.1f} ({scorecard.security.trend})")
495
+ print(
496
+ f" Traceability: {scorecard.traceability.score:.1f} ({scorecard.traceability.trend})"
497
+ )
498
+ print(f" Records: {scorecard.record_count}")
499
+
500
+ return 0
501
+
502
+
503
+ def _cmd_trust_badge(args: argparse.Namespace) -> int:
504
+ """``spanforge trust badge`` - generate T.R.U.S.T. badge SVG."""
505
+ from spanforge.sdk import sf_trust
506
+
507
+ project_id = getattr(args, "project_id", "") or ""
508
+ output = getattr(args, "output", None)
509
+
510
+ try:
511
+ badge = sf_trust.get_badge(project_id=project_id or None)
512
+ except Exception as exc:
513
+ print(f"error: {exc}", file=sys.stderr)
514
+ return 1
515
+
516
+ if output:
517
+ Path(output).write_text(badge.svg, encoding="utf-8")
518
+ print(f"Badge written to {output} (score={badge.overall:.1f}, {badge.colour_band})")
519
+ else:
520
+ print(badge.svg)
521
+
522
+ return 0
523
+
524
+
525
+ def _cmd_trust_gate(args: argparse.Namespace) -> int:
526
+ """``spanforge trust gate`` - run composite trust gate."""
527
+ from spanforge.sdk import sf_gate, sf_trust
528
+
529
+ project_id = getattr(args, "project_id", "") or ""
530
+ min_score = getattr(args, "min_score", 60.0)
531
+ fmt = getattr(args, "format", "text")
532
+
533
+ try:
534
+ scorecard = sf_trust.get_scorecard(project_id=project_id or None)
535
+ except Exception as exc:
536
+ print(f"error: {exc}", file=sys.stderr)
537
+ return 1
538
+
539
+ failures: list[str] = []
540
+ if scorecard.overall_score < min_score:
541
+ failures.append(f"T.R.U.S.T. score {scorecard.overall_score} < minimum {min_score}")
542
+
543
+ try:
544
+ trust_gate = sf_gate.run_trust_gate(project_id=project_id)
545
+ if not trust_gate.pass_:
546
+ failures.extend(trust_gate.failures)
547
+ except Exception as exc:
548
+ failures.append(f"Trust gate error: {exc}")
549
+
550
+ verdict = "PASS" if not failures else "FAIL"
551
+
552
+ if fmt == "json":
553
+ import json as _json
554
+
555
+ print(
556
+ _json.dumps(
557
+ {
558
+ "pass": not failures,
559
+ "verdict": verdict,
560
+ "overall_score": scorecard.overall_score,
561
+ "colour_band": scorecard.colour_band,
562
+ "failures": failures,
563
+ },
564
+ indent=2,
565
+ )
566
+ )
567
+ else:
568
+ band = scorecard.colour_band.upper()
569
+ print(f"Composite Trust Gate: {verdict}")
570
+ print(f" Score: {scorecard.overall_score:.1f} [{band}] (min: {min_score})")
571
+ if failures:
572
+ for failure in failures:
573
+ print(f" FAIL: {failure}")
574
+ else:
575
+ print(" All checks passed.")
576
+
577
+ return 0 if not failures else 1
578
+
579
+
580
+ def _cmd_config_validate(args: argparse.Namespace) -> int:
581
+ """Validate a ``.halluccheck.toml`` config file against the v6.0 schema."""
582
+ from spanforge.sdk._exceptions import SFConfigError
583
+ from spanforge.sdk.config import load_config_file, validate_config
584
+
585
+ file_path = getattr(args, "file", None)
586
+
587
+ try:
588
+ block = load_config_file(file_path)
589
+ except SFConfigError as exc:
590
+ print(f"error: {exc}", file=sys.stderr)
591
+ return 2
592
+
593
+ errors = validate_config(block)
594
+ if errors:
595
+ print(f"Config validation failed ({len(errors)} error(s)):")
596
+ for err in errors:
597
+ print(f" - {err}")
598
+ return 1
599
+
600
+ source = file_path or "(auto-discovered .halluccheck.toml or defaults)"
601
+ print(f"[✓] Config is valid: {source}")
602
+ return 0
603
+
604
+
605
+ def _cmd_doctor(_args: argparse.Namespace) -> int:
606
+ """Run environment health checks."""
607
+ from spanforge.sdk import (
608
+ sf_alert,
609
+ sf_audit,
610
+ sf_cec,
611
+ sf_enterprise,
612
+ sf_gate,
613
+ sf_identity,
614
+ sf_observe,
615
+ sf_pii,
616
+ sf_secrets,
617
+ sf_security,
618
+ sf_trust,
619
+ )
620
+ from spanforge.sdk.config import load_config_file, validate_config
621
+
622
+ pass_marker = "[✓]" # nosec B105
623
+ fail_marker = "[✗]"
624
+ warn_marker = "[!]"
625
+ failures = 0
626
+
627
+ print("SpanForge Doctor")
628
+ print("=" * 40)
629
+
630
+ print("\n--- Configuration ---")
631
+ try:
632
+ cfg = load_config_file()
633
+ errors = validate_config(cfg)
634
+ if errors:
635
+ for err in errors:
636
+ print(f" {fail_marker} {err}")
637
+ failures += len(errors)
638
+ else:
639
+ print(f" {pass_marker} Config valid")
640
+ if getattr(cfg, "sandbox", False):
641
+ print(f" {warn_marker} Sandbox mode is ENABLED")
642
+ except FileNotFoundError:
643
+ print(f" {warn_marker} No spanforge.toml found (using defaults)")
644
+ except Exception as exc:
645
+ print(f" {fail_marker} Config load error: {exc}")
646
+ failures += 1
647
+
648
+ print("\n--- Service Status ---")
649
+ services = [
650
+ ("sf_identity", sf_identity),
651
+ ("sf_pii", sf_pii),
652
+ ("sf_secrets", sf_secrets),
653
+ ("sf_audit", sf_audit),
654
+ ("sf_observe", sf_observe),
655
+ ("sf_gate", sf_gate),
656
+ ("sf_cec", sf_cec),
657
+ ("sf_alert", sf_alert),
658
+ ("sf_trust", sf_trust),
659
+ ("sf_enterprise", sf_enterprise),
660
+ ("sf_security", sf_security),
661
+ ]
662
+ for name, svc in services:
663
+ try:
664
+ status = cast("Any", svc).get_status()
665
+ service_state = (
666
+ getattr(status, "status", None) if not isinstance(status, dict) else status.get("status")
667
+ )
668
+ if service_state == "ok":
669
+ print(f" {pass_marker} {name}: ok")
670
+ else:
671
+ print(f" {warn_marker} {name}: {service_state}")
672
+ except Exception as exc:
673
+ print(f" {fail_marker} {name}: {exc}")
674
+ failures += 1
675
+
676
+ print("\n--- PII Engine ---")
677
+ try:
678
+ pii_status = sf_pii.get_status()
679
+ types_loaded = getattr(pii_status, "entity_types_loaded", [])
680
+ if types_loaded:
681
+ print(f" {pass_marker} {len(types_loaded)} entity type(s) loaded")
682
+ else:
683
+ print(f" {warn_marker} No PII entity types loaded (Presidio not available?)")
684
+ except Exception as exc:
685
+ print(f" {fail_marker} PII status error: {exc}")
686
+ failures += 1
687
+
688
+ print("\n--- Compliance Posture ---")
689
+ try:
690
+ from datetime import datetime, timezone
691
+
692
+ from spanforge.core.compliance_mapping import ComplianceMappingEngine
693
+
694
+ engine = ComplianceMappingEngine()
695
+ store_events = engine._load_from_store()
696
+ if store_events:
697
+ _today = datetime.now(timezone.utc)
698
+ _from = _today.strftime("%Y-%m-01")
699
+ _to = _today.strftime("%Y-%m-%d")
700
+ try:
701
+ pkg = engine.generate_evidence_package(
702
+ model_id="*",
703
+ framework="eu_ai_act",
704
+ from_date=_from,
705
+ to_date=_to,
706
+ audit_events=store_events,
707
+ )
708
+ passing = sum(1 for r in pkg.attestation.clauses if r.status.value == "pass")
709
+ total = len(pkg.attestation.clauses)
710
+ gaps = pkg.gap_report.gap_clause_ids
711
+ partials = pkg.gap_report.partial_clause_ids
712
+ overall = pkg.attestation.overall_status.value.upper()
713
+ icon = pass_marker if overall == "PASS" else (warn_marker if not gaps else fail_marker)
714
+ print(f" {icon} EU AI Act posture: {passing}/{total} clauses passing — {overall}")
715
+ if gaps:
716
+ print(f" {fail_marker} Gaps: {', '.join(gaps)}")
717
+ if partials:
718
+ print(f" {warn_marker} Partial: {', '.join(partials)}")
719
+ print(" Run `spanforge compliance readiness` for a full pre-audit checklist.")
720
+ except Exception as _ce:
721
+ print(f" {warn_marker} Could not evaluate compliance posture: {_ce}")
722
+ else:
723
+ print(f" {warn_marker} No events in store — instrument your first LLM call to see posture.")
724
+ print(" Run `spanforge compliance readiness` for a pre-audit config check.")
725
+ except Exception as _exc:
726
+ print(f" {warn_marker} Compliance posture check unavailable: {_exc}")
727
+
728
+ print("\n" + "=" * 40)
729
+ if failures == 0:
730
+ print(f"{pass_marker} All checks passed.")
731
+ return 0
732
+
733
+ print(f"{fail_marker} {failures} check(s) failed.")
734
+ return 1
735
+
736
+
737
+ def _cmd_operator_inspect(args: argparse.Namespace) -> int:
738
+ """``spanforge operator inspect`` - inspect one operator workflow trace."""
739
+ from spanforge.sdk import sf_operator
740
+
741
+ fmt = getattr(args, "format", "text")
742
+ trace_id = getattr(args, "trace_id", "")
743
+
744
+ try:
745
+ workflow = sf_operator.inspect_trace(trace_id)
746
+ except Exception as exc:
747
+ print(f"error: {exc}", file=sys.stderr)
748
+ return 1
749
+
750
+ if fmt == "json":
751
+ print(json.dumps(workflow.to_dict(), indent=2))
752
+ else:
753
+ print(f"Trace: {workflow.trace_id}")
754
+ print(f"Outcome: {workflow.outcome}")
755
+ print(f"Summary: {workflow.summary}")
756
+ print(f"Policy decisions: {len(workflow.policy_decisions)}")
757
+ print(f"Grounding results: {len(workflow.grounding_results)}")
758
+ print(f"Scope decisions: {len(workflow.scope_decisions)}")
759
+ print(f"RBAC decisions: {len(workflow.rbac_decisions)}")
760
+ print(f"Lineage records: {len(workflow.lineage_records)}")
761
+ print(f"Signed audit records: {len(workflow.audit_records)}")
762
+
763
+ return 0
764
+
765
+
766
+ def _cmd_operator_export(args: argparse.Namespace) -> int:
767
+ """``spanforge operator export`` - export a signed trace evidence package."""
768
+ from spanforge.sdk import sf_operator
769
+
770
+ trace_id = getattr(args, "trace_id", "")
771
+ output = getattr(args, "output", None)
772
+ fmt = getattr(args, "format", "text")
773
+
774
+ try:
775
+ package = sf_operator.export_package(trace_id, output_path=output)
776
+ except Exception as exc:
777
+ print(f"error: {exc}", file=sys.stderr)
778
+ return 1
779
+
780
+ if fmt == "json":
781
+ print(json.dumps(package.to_dict(), indent=2))
782
+ elif output:
783
+ print(f"Evidence package written to {output}")
784
+ else:
785
+ print(f"Package: {package.package_id}")
786
+ print(f"Trace: {package.trace_id}")
787
+ print(f"Outcome: {package.outcome}")
788
+ print(f"Records: {package.exported_records}")
789
+ print(f"Signature: {package.signature}")
790
+
791
+ return 0