Download src/hdk/cli.py from JakCwi/HDK-audit: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/JakCwi/HDK-audit/resolve/main/src/hdk/cli.py
- Command line
-
hf download hf://JakCwi/HDK-audit/src/hdk/cli.py
-
curl -L -o cli.py https://huggingface.co/JakCwi/HDK-audit/resolve/main/src/hdk/cli.py
12.3 kB
| # SPDX-License-Identifier: CC-BY-NC-4.0 | |
| # Copyright (c) 2026 RE::AI Strategy | |
| """HDK command line interface (installed as `hdk-audit`).""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| def _get_db_path(args) -> str: | |
| """Get DB path from args or default.""" | |
| if hasattr(args, "db") and args.db: | |
| return args.db | |
| default = Path.home() / ".hdk" / "dag.sqlite" | |
| return str(default) | |
| def _make_canary(dag, args): | |
| """Canary with the key from --canary-key-file, $HDK_CANARY_KEY or $HDK_CANARY_KEY_FILE.""" | |
| from hdk.core.canary import CanaryLayer | |
| key_file = getattr(args, "canary_key_file", None) | |
| return CanaryLayer(dag, key_file=key_file) | |
| def cmd_verify(args): | |
| """Full integrity verification.""" | |
| from hdk.core.dag import DAGStorage | |
| db_path = _get_db_path(args) | |
| if not Path(db_path).exists() and db_path != ":memory:": | |
| print(f"Database not found: {db_path}") | |
| sys.exit(2) | |
| dag = DAGStorage(db_path) | |
| canary = _make_canary(dag, args) | |
| dag.set_node_key(canary.node_key()) # 0.1.2: woven nodes' hashes are HMACs under the key | |
| dag_valid, dag_errors = dag.verify_integrity() | |
| node_count = dag.get_node_count() | |
| un_anchored = dag.get_nodes_since_anchor(-1) | |
| last_anchor = dag.get_last_anchor() | |
| # compares the stored counter with its commitment, the last anchored checkpoint and | |
| # the generations woven into nodes; None = disabled (no key) | |
| canary_valid, canary_reason = canary.check() | |
| # Output | |
| print("┌─────────────────────────────────────┐") | |
| print("│ HDK INTEGRITY VERIFICATION │") | |
| print("├─────────────────────────────────────┤") | |
| dag_status = "✓ VALID" if dag_valid else "✗ INVALID" | |
| print(f"│ DAG Integrity: {dag_status:<18}│") | |
| print(f"│ Node Count: {node_count:<18}│") | |
| canary_status = {True: "✓ GREEN", False: "✗ TAMPERED", None: "- DISABLED"}[canary_valid] | |
| print(f"│ Canary Status: {canary_status:<18}│") | |
| if last_anchor: | |
| print(f"│ Last Anchor: #{last_anchor.anchor_sequence:<17}│") | |
| print(f"│ Topic: {last_anchor.topic_id:<18}│") | |
| else: | |
| print(f"│ Last Anchor: {'None':<18}│") | |
| print(f"│ Un-anchored: {len(un_anchored):<18}│") | |
| print("└─────────────────────────────────────┘") | |
| if canary_valid is not True: | |
| print(f"\nCanary: {canary_reason}") | |
| if canary.node_key() is None: | |
| print("\nNo canary key: entries not yet anchored are protected by anchoring only " | |
| "(they can be rewritten with recomputed hashes).") | |
| if dag.unchecked_keyed: | |
| print(f"\n{dag.unchecked_keyed} keyed node(s) not checked (no key, or a rotated one).") | |
| if dag_errors: | |
| print("\nErrors:") | |
| for err in dag_errors: | |
| print(f" ✗ {err}") | |
| dag.close() | |
| sys.exit(0 if dag_valid and canary_valid is not False else 1) | |
| def cmd_inspect(args): | |
| """Show full details of a specific node.""" | |
| from hdk.core.dag import DAGStorage | |
| db_path = _get_db_path(args) | |
| dag = DAGStorage(db_path) | |
| node = dag.get_node_by_id(args.node_id) | |
| if not node: | |
| node = dag.get_node(args.node_id) | |
| if not node: | |
| print(f"Node not found: {args.node_id}") | |
| dag.close() | |
| sys.exit(2) | |
| print(f"Node ID: {node.id}") | |
| print(f"Level: {node.level.value}") | |
| print(f"Event Type: {node.event_type.value}") | |
| print(f"Timestamp: {node.timestamp.isoformat()}") | |
| print(f"Content Hash: {node.content_hash}") | |
| print(f"Self Hash: {node.self_hash}") | |
| print(f"Parent Chain: {node.parent_chain}") | |
| print(f"Canary Gen: {node.canary_generation}") | |
| print(f"Model: {node.model or 'N/A'}") | |
| print(f"Provider: {node.provider or 'N/A'}") | |
| print(f"Node Hash: {node.node_hash}") | |
| dag.close() | |
| def cmd_tree(args): | |
| """Show DAG hierarchy as tree.""" | |
| from hdk.core.dag import DAGStorage | |
| db_path = _get_db_path(args) | |
| dag = DAGStorage(db_path) | |
| depth = args.depth if hasattr(args, "depth") and args.depth else 3 | |
| cursor = dag._conn.execute("SELECT * FROM nodes ORDER BY timestamp ASC") | |
| nodes = [dag._row_to_node(row) for row in cursor.fetchall()] | |
| if not nodes: | |
| print("(empty DAG)") | |
| dag.close() | |
| return | |
| # Simple tree display: indent by level | |
| level_indent = { | |
| "PROJECT": 0, "TAB": 1, "CONV": 2, | |
| "QUERY": 3, "RESPONSE": 4, "EVENT": 2, | |
| } | |
| for node in nodes[:50]: # Limit display | |
| indent = level_indent.get(node.level.value, 2) | |
| prefix = " " * indent + "├── " | |
| model_info = f" [{node.model}]" if node.model else "" | |
| print(f"{prefix}{node.level.value} {node.id[:12]}...{model_info}") | |
| if len(nodes) > 50: | |
| print(f"\n ... and {len(nodes) - 50} more nodes") | |
| dag.close() | |
| def cmd_anchor(args): | |
| """Show anchor history or force anchor.""" | |
| from hdk.core.dag import DAGStorage | |
| db_path = _get_db_path(args) | |
| dag = DAGStorage(db_path) | |
| if hasattr(args, "now") and args.now: | |
| import asyncio | |
| from hdk.config import HederaConfig | |
| from hdk.hedera.anchor import HederaAnchorService | |
| # Hedera credentials from $HDK_HEDERA_OPERATOR_ID / _OPERATOR_KEY / _TOPIC_ID | |
| # (and _NETWORK); without them the anchor is recorded locally only. | |
| canary = _make_canary(dag, args) | |
| service = HederaAnchorService(HederaConfig.from_env(), dag, canary) | |
| async def _run(): | |
| try: | |
| return await service.submit_anchor(force=True) | |
| finally: | |
| await service.close() | |
| anchor = asyncio.run(_run()) | |
| if anchor: | |
| print(f"Anchor #{anchor.anchor_sequence} created: {anchor.merkle_root[:16]}...") | |
| print(f"Status: {anchor.status}") | |
| if anchor.sequence_number is not None: | |
| print(f"Hedera: topic {anchor.topic_id} sequence {anchor.sequence_number} " | |
| f"consensus {anchor.consensus_timestamp or 'unknown'}") | |
| else: | |
| print("No un-anchored nodes.") | |
| dag.close() | |
| return | |
| # Show anchor history | |
| anchors = dag.get_anchor_history(limit=20) | |
| if not anchors: | |
| print("No anchors yet.") | |
| dag.close() | |
| return | |
| for a in reversed(anchors): | |
| status = "✓ on Hedera" if a.sequence_number is not None else "local only" | |
| print(f" #{a.anchor_sequence} {a.timestamp.isoformat()[:19]} " | |
| f"nodes={a.node_count} {status} {a.topic_id}") | |
| dag.close() | |
| def cmd_export(args): | |
| """Export DAG as JSON.""" | |
| from hdk.core.dag import DAGStorage | |
| db_path = _get_db_path(args) | |
| dag = DAGStorage(db_path) | |
| print(dag.export_dag(format=args.format if hasattr(args, "format") else "json")) | |
| dag.close() | |
| def cmd_stats(args): | |
| """Show DAG statistics.""" | |
| from hdk.core.dag import DAGStorage | |
| db_path = _get_db_path(args) | |
| dag = DAGStorage(db_path) | |
| total = dag.get_node_count() | |
| # Count by type | |
| counts = {} | |
| for event_type in ["LLM_QUERY", "LLM_RESPONSE", "PASTE_EXTERNAL", | |
| "PUBLISH", "AI_INSERT", "SESSION_START", "SESSION_END", "CUSTOM"]: | |
| cursor = dag._conn.execute( | |
| "SELECT COUNT(*) FROM nodes WHERE event_type = ?", (event_type,) | |
| ) | |
| counts[event_type] = cursor.fetchone()[0] | |
| # Count by level | |
| level_counts = {} | |
| for level in ["PROJECT", "TAB", "CONV", "QUERY", "RESPONSE", "EVENT"]: | |
| cursor = dag._conn.execute( | |
| "SELECT COUNT(*) FROM nodes WHERE level = ?", (level,) | |
| ) | |
| level_counts[level] = cursor.fetchone()[0] | |
| anchor_cursor = dag._conn.execute("SELECT COUNT(*) FROM anchors") | |
| anchor_count = anchor_cursor.fetchone()[0] | |
| print(f"Total Nodes: {total}") | |
| print(f"Projects: {level_counts.get('PROJECT', 0)}") | |
| print(f"Conversations: {level_counts.get('CONV', 0)}") | |
| print(f"LLM Queries: {counts.get('LLM_QUERY', 0)}") | |
| print(f"LLM Responses: {counts.get('LLM_RESPONSE', 0)}") | |
| print(f"Pastes: {counts.get('PASTE_EXTERNAL', 0)}") | |
| print(f"Publishes: {counts.get('PUBLISH', 0)}") | |
| print(f"Anchors: {anchor_count}") | |
| # DB size | |
| db_file = Path(db_path) | |
| if db_file.exists(): | |
| size_kb = db_file.stat().st_size / 1024 | |
| print(f"DB Size: {size_kb:.1f} KB") | |
| dag.close() | |
| def cmd_certificate(args): | |
| """Generate certificate for a document.""" | |
| from hdk.certificate import CertificateGenerator | |
| from hdk.core.dag import DAGStorage | |
| from hdk.core.hash_engine import HashEngine | |
| db_path = _get_db_path(args) | |
| dag = DAGStorage(db_path) | |
| engine = HashEngine() | |
| doc_path = Path(args.document) | |
| if not doc_path.exists(): | |
| print(f"File not found: {args.document}") | |
| dag.close() | |
| sys.exit(2) | |
| content = doc_path.read_text(encoding="utf-8") | |
| from hdk.config import HederaConfig | |
| gen = CertificateGenerator(dag, engine, network=HederaConfig.from_env().network) | |
| cert = gen.generate(content, "cli-user") | |
| print(gen.to_markdown(cert)) | |
| dag.close() | |
| def main(): | |
| """Entry point for the `hdk-audit` CLI command.""" | |
| parser = argparse.ArgumentParser( | |
| prog="hdk-audit", | |
| description="HDK — Hedera Detection Key. Tamper-evident audit trail for LLM calls.", | |
| ) | |
| parser.add_argument("--version", action="store_true", help="Show version") | |
| subparsers = parser.add_subparsers(dest="command") | |
| # verify | |
| p_verify = subparsers.add_parser("verify", help="Verify DAG integrity + canary state") | |
| p_verify.add_argument("--db", help="Database path") | |
| p_verify.add_argument("--canary-key-file", help="Canary key file (else $HDK_CANARY_KEY[_FILE])") | |
| # inspect | |
| p_inspect = subparsers.add_parser("inspect", help="Show details of a specific node") | |
| p_inspect.add_argument("node_id", help="Node ID or hash") | |
| p_inspect.add_argument("--db", help="Database path") | |
| # tree | |
| p_tree = subparsers.add_parser("tree", help="Show DAG hierarchy") | |
| p_tree.add_argument("--depth", type=int, default=3, help="Tree depth") | |
| p_tree.add_argument("--db", help="Database path") | |
| # anchor | |
| p_anchor = subparsers.add_parser("anchor", help="Anchor management") | |
| p_anchor.add_argument("--status", action="store_true", help="Show anchor history") | |
| p_anchor.add_argument("--now", action="store_true", help="Force immediate anchor") | |
| p_anchor.add_argument("--db", help="Database path") | |
| p_anchor.add_argument("--canary-key-file", help="Canary key file (else $HDK_CANARY_KEY[_FILE])") | |
| # export | |
| p_export = subparsers.add_parser("export", help="Export DAG") | |
| p_export.add_argument("--format", choices=["json", "csv"], default="json") | |
| p_export.add_argument("--db", help="Database path") | |
| # certificate | |
| p_cert = subparsers.add_parser("certificate", help="Generate certificate") | |
| p_cert.add_argument("document", help="Document file path") | |
| p_cert.add_argument("--db", help="Database path") | |
| # stats | |
| p_stats = subparsers.add_parser("stats", help="Show DAG statistics") | |
| p_stats.add_argument("--db", help="Database path") | |
| # version | |
| subparsers.add_parser("version", help="Show version info") | |
| args = parser.parse_args() | |
| if args.version or args.command == "version": | |
| from hdk import __version__ | |
| print(__version__) | |
| return | |
| if args.command is None: | |
| parser.print_help() | |
| return | |
| commands = { | |
| "verify": cmd_verify, | |
| "inspect": cmd_inspect, | |
| "tree": cmd_tree, | |
| "anchor": cmd_anchor, | |
| "export": cmd_export, | |
| "certificate": cmd_certificate, | |
| "stats": cmd_stats, | |
| } | |
| handler = commands.get(args.command) | |
| if handler: | |
| handler(args) | |
| else: | |
| parser.print_help() | |
| sys.exit(2) | |
| if __name__ == "__main__": | |
| main() | |