loki-mode 7.49.0 → 7.51.0

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.
@@ -513,30 +513,25 @@ def _file_has_integrity(log_file: str) -> bool:
513
513
  return False
514
514
 
515
515
 
516
- def verify_all_logs() -> dict:
517
- """v7.7.15: verify the entire audit chain across all rotated log files.
516
+ def verify_all_logs_in_dir(audit_dir) -> dict:
517
+ """Verify the entire audit chain across all rotated log files in
518
+ an explicit directory.
518
519
 
519
- Walks `AUDIT_DIR/audit-*.jsonl` in chronological order, threading
520
- the chain hash from one file to the next via `start_hash`. Skips
521
- files from the pre-integrity era (files whose first entry has no
522
- `_integrity_hash` field, because integrity hashing was introduced
523
- after some audit logs already existed).
520
+ This is the directory-parameterized core of :func:`verify_all_logs`.
521
+ The default :func:`verify_all_logs` delegates here with the module
522
+ ``AUDIT_DIR`` so existing callers are unaffected. The explicit-dir
523
+ form lets the unified cross-chain verifier (see ``src/audit/crosslink.js``)
524
+ validate an arbitrary audit directory without mutating module globals.
525
+
526
+ Args:
527
+ audit_dir: Path (str or pathlib.Path) to a directory containing
528
+ ``audit-*.jsonl`` files.
524
529
 
525
530
  Returns:
526
- A dict with:
527
- - valid (bool): True if the entire cross-file chain is intact.
528
- - files_checked (int): Count of integrity-bearing files inspected.
529
- - files_skipped (int): Count of pre-integrity files skipped.
530
- - entries_checked (int): Total entries verified across all files.
531
- - first_tampered_file (str | None): Path to the first file
532
- whose chain broke, or None if valid.
533
- - first_tampered_line (int | None): 1-based line number in
534
- that file where the chain broke, or None if valid.
535
- - genesis_file (str | None): Path to the first integrity-bearing
536
- log file (the chain's genesis on this machine), or None if
537
- no integrity-bearing files exist.
531
+ Same shape as :func:`verify_all_logs`.
538
532
  """
539
- if not AUDIT_DIR.exists():
533
+ audit_dir = Path(audit_dir)
534
+ if not audit_dir.exists():
540
535
  return {"valid": True, "files_checked": 0, "files_skipped": 0,
541
536
  "entries_checked": 0, "first_tampered_file": None,
542
537
  "first_tampered_line": None, "genesis_file": None}
@@ -547,7 +542,7 @@ def verify_all_logs() -> dict:
547
542
  # would break chain ordering and false-negative on any user who hit
548
543
  # size-based rotation. Sort by mtime instead -- mirrors what
549
544
  # `_cleanup_old_logs` already does at line 178.
550
- files = sorted(AUDIT_DIR.glob("audit-*.jsonl"), key=lambda p: p.stat().st_mtime)
545
+ files = sorted(audit_dir.glob("audit-*.jsonl"), key=lambda p: p.stat().st_mtime)
551
546
  prev_hash = "0" * 64
552
547
  total_entries = 0
553
548
  files_checked = 0
@@ -582,3 +577,189 @@ def verify_all_logs() -> dict:
582
577
  "first_tampered_line": None,
583
578
  "genesis_file": genesis_file,
584
579
  }
580
+
581
+
582
+ def verify_all_logs() -> dict:
583
+ """v7.7.15: verify the entire audit chain across all rotated log files.
584
+
585
+ Walks `AUDIT_DIR/audit-*.jsonl` in chronological order, threading
586
+ the chain hash from one file to the next via `start_hash`. Skips
587
+ files from the pre-integrity era (files whose first entry has no
588
+ `_integrity_hash` field, because integrity hashing was introduced
589
+ after some audit logs already existed).
590
+
591
+ Returns:
592
+ A dict with:
593
+ - valid (bool): True if the entire cross-file chain is intact.
594
+ - files_checked (int): Count of integrity-bearing files inspected.
595
+ - files_skipped (int): Count of pre-integrity files skipped.
596
+ - entries_checked (int): Total entries verified across all files.
597
+ - first_tampered_file (str | None): Path to the first file
598
+ whose chain broke, or None if valid.
599
+ - first_tampered_line (int | None): 1-based line number in
600
+ that file where the chain broke, or None if valid.
601
+ - genesis_file (str | None): Path to the first integrity-bearing
602
+ log file (the chain's genesis on this machine), or None if
603
+ no integrity-bearing files exist.
604
+ """
605
+ return verify_all_logs_in_dir(AUDIT_DIR)
606
+
607
+
608
+ def compute_chain_tip_in_dir(audit_dir) -> dict:
609
+ """Return the current tip (last hash) of the Python audit chain in
610
+ an explicit directory, plus a verification verdict for that chain.
611
+
612
+ Used by the unified cross-chain verifier to anchor the Python chain
613
+ state into the JS (``src/audit/log.js``) tamper-evident chain via a
614
+ cross-link record, and to reconcile a previously recorded anchor
615
+ against the live chain.
616
+
617
+ Args:
618
+ audit_dir: Path (str or pathlib.Path) to the Python audit dir.
619
+
620
+ Returns:
621
+ A dict with:
622
+ - genesis (str): The genesis hash for this chain ("0"*64).
623
+ - tip_hash (str): The last integrity hash, or the genesis hash
624
+ if the chain is empty.
625
+ - entries (int): Total integrity-bearing entries in the chain.
626
+ - valid (bool): Whether the chain verifies end-to-end.
627
+ - chain_id (str): Stable identifier for this chain family.
628
+ """
629
+ result = verify_all_logs_in_dir(audit_dir)
630
+ audit_dir = Path(audit_dir)
631
+ genesis = "0" * 64
632
+ tip_hash = genesis
633
+ if audit_dir.exists():
634
+ files = sorted(audit_dir.glob("audit-*.jsonl"), key=lambda p: p.stat().st_mtime)
635
+ prev = genesis
636
+ for log_file in files:
637
+ if not _file_has_integrity(str(log_file)):
638
+ continue
639
+ r = verify_log_integrity(str(log_file), start_hash=prev)
640
+ prev = r.get("last_hash", prev)
641
+ tip_hash = prev
642
+ return {
643
+ "genesis": genesis,
644
+ "tip_hash": tip_hash,
645
+ "entries": result.get("entries_checked", 0),
646
+ "valid": bool(result.get("valid", False)),
647
+ "chain_id": "loki-dashboard-audit",
648
+ }
649
+
650
+
651
+ def compute_prefix_hash_in_dir(audit_dir, n_entries: int) -> dict:
652
+ """Recompute the dashboard chain hash after exactly the first
653
+ ``n_entries`` integrity-bearing entries, walking files in mtime
654
+ order across rotations.
655
+
656
+ This is what lets the unified cross-chain verifier distinguish
657
+ legitimate append-only GROWTH from TAMPER. A cross-link anchor pins
658
+ ``(tip_hash, entries)`` at link time; later the live chain may have
659
+ grown. Reconciliation recomputes the hash of the first ``n_entries``
660
+ (the anchored prefix) and checks it still reproduces the anchored
661
+ ``tip_hash``. Growth keeps the prefix intact (reproducible); any
662
+ mutation at-or-before the anchor, or truncation below it, makes the
663
+ prefix unreproducible.
664
+
665
+ Args:
666
+ audit_dir: Path (str or pathlib.Path) to the Python audit dir.
667
+ n_entries: Number of leading integrity-bearing entries to hash.
668
+
669
+ Returns:
670
+ A dict with:
671
+ - found (bool): True if at least ``n_entries`` entries exist
672
+ and the prefix was hashed without a chain break inside it.
673
+ - prefix_hash (str): The chain hash after ``n_entries`` entries
674
+ (or the running hash reached if fewer entries exist / a break
675
+ occurred -- in which case ``found`` is False).
676
+ - entries_available (int): Total integrity-bearing entries seen.
677
+ """
678
+ audit_dir = Path(audit_dir)
679
+ genesis = "0" * 64
680
+ if n_entries <= 0:
681
+ return {"found": True, "prefix_hash": genesis, "entries_available": 0}
682
+ if not audit_dir.exists():
683
+ return {"found": False, "prefix_hash": genesis, "entries_available": 0}
684
+ files = sorted(audit_dir.glob("audit-*.jsonl"), key=lambda p: p.stat().st_mtime)
685
+ prev_hash = genesis
686
+ seen = 0
687
+ started = False
688
+ for log_file in files:
689
+ if not started and not _file_has_integrity(str(log_file)):
690
+ continue
691
+ started = True
692
+ try:
693
+ with open(log_file, "r") as f:
694
+ for line in f:
695
+ line = line.strip()
696
+ if not line:
697
+ continue
698
+ try:
699
+ entry = json.loads(line)
700
+ except json.JSONDecodeError:
701
+ return {"found": False, "prefix_hash": prev_hash,
702
+ "entries_available": seen}
703
+ stored_hash = entry.pop("_integrity_hash", None)
704
+ if stored_hash is None:
705
+ return {"found": False, "prefix_hash": prev_hash,
706
+ "entries_available": seen}
707
+ entry_json = json.dumps(entry, sort_keys=True, default=str)
708
+ expected = _compute_chain_hash(entry_json, prev_hash)
709
+ if stored_hash != expected:
710
+ # Chain broke inside the prefix -> not reproducible.
711
+ return {"found": False, "prefix_hash": prev_hash,
712
+ "entries_available": seen}
713
+ prev_hash = stored_hash
714
+ seen += 1
715
+ if seen == n_entries:
716
+ return {"found": True, "prefix_hash": prev_hash,
717
+ "entries_available": seen}
718
+ except OSError:
719
+ return {"found": False, "prefix_hash": prev_hash,
720
+ "entries_available": seen}
721
+ # Fewer than n_entries entries exist (truncation below the anchor).
722
+ return {"found": False, "prefix_hash": prev_hash, "entries_available": seen}
723
+
724
+
725
+ def _unified_cli() -> int:
726
+ """Tiny CLI shim so the Node-side unified verifier (or an operator)
727
+ can fetch the Python chain tip / verdict for a given directory as
728
+ JSON. Invoked as:
729
+
730
+ python3 dashboard/audit.py tip <audit_dir>
731
+ python3 dashboard/audit.py verify <audit_dir>
732
+
733
+ Prints a single JSON object to stdout. Returns process exit code 0
734
+ on a valid chain, 1 on an invalid chain, 2 on usage error.
735
+ """
736
+ argv = sys.argv[1:]
737
+ if len(argv) < 2 or argv[0] not in ("tip", "verify", "prefix"):
738
+ print(json.dumps(
739
+ {"error": "usage: audit.py {tip|verify} <audit_dir> "
740
+ "| prefix <audit_dir> <n_entries>"}))
741
+ return 2
742
+ cmd, audit_dir = argv[0], argv[1]
743
+ if cmd == "tip":
744
+ out = compute_chain_tip_in_dir(audit_dir)
745
+ print(json.dumps(out))
746
+ return 0 if out.get("valid", False) else 1
747
+ if cmd == "prefix":
748
+ if len(argv) < 3:
749
+ print(json.dumps({"error": "prefix requires <n_entries>"}))
750
+ return 2
751
+ try:
752
+ n = int(argv[2])
753
+ except ValueError:
754
+ print(json.dumps({"error": "n_entries must be an integer"}))
755
+ return 2
756
+ out = compute_prefix_hash_in_dir(audit_dir, n)
757
+ print(json.dumps(out))
758
+ return 0 if out.get("found", False) else 1
759
+ out = verify_all_logs_in_dir(audit_dir)
760
+ print(json.dumps(out))
761
+ return 0 if out.get("valid", False) else 1
762
+
763
+
764
+ if __name__ == "__main__":
765
+ sys.exit(_unified_cli())
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.49.0
5
+ **Version:** v7.51.0
6
6
 
7
7
  ---
8
8
 
@@ -396,7 +396,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
396
396
  # Run Loki Mode in Docker (Claude provider, API-key auth)
397
397
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
398
398
  -v $(pwd):/workspace -w /workspace \
399
- asklokesh/loki-mode:7.49.0 start ./my-spec.md
399
+ asklokesh/loki-mode:7.51.0 start ./my-spec.md
400
400
  ```
401
401
 
402
402
  ##### docker compose + .env (no host install)
@@ -72,6 +72,67 @@ loki syslog test --message "Test event from Loki Mode"
72
72
  tail -f /var/log/loki-mode.log
73
73
  ```
74
74
 
75
+ ## Event Export Module (CEF + Splunk HEC)
76
+
77
+ In addition to syslog forwarding, Loki Mode ships a programmatic exporter for
78
+ audit/security events at `src/observability/siem-export.js`. It provides two
79
+ well-specified, vendor-agnostic formats and an SSRF-safe HEC sender.
80
+
81
+ ### Zero egress unless configured
82
+
83
+ The module follows the same gate as the OTEL bridge: nothing leaves the host
84
+ unless an endpoint env var is set. `createHECSenderFromEnv()` returns `null`
85
+ when `LOKI_SPLUNK_HEC_URL` is unset, so there is no code path to the network.
86
+ Endpoint URLs are validated to be `http:`/`https:` only (the same SSRF guard
87
+ the OTLP exporter uses), so a stray `file://` or `gopher://` endpoint is
88
+ rejected before any request is built.
89
+
90
+ ### Auto-detected environment variables
91
+
92
+ | Variable | Required | Description |
93
+ |----------|----------|-------------|
94
+ | `LOKI_SPLUNK_HEC_URL` | enables HEC | Splunk HEC collector URL. Presence enables the sender. |
95
+ | `LOKI_SPLUNK_HEC_TOKEN` | no | HEC auth token (sent as `Authorization: Splunk <token>`). |
96
+ | `LOKI_SPLUNK_HEC_INDEX` | no | Target Splunk index. |
97
+ | `LOKI_SPLUNK_HEC_SOURCETYPE` | no | Sourcetype (default `loki:audit`). |
98
+ | `LOKI_CEF_VENDOR` / `LOKI_CEF_PRODUCT` | no | Override CEF vendor/product header fields. |
99
+
100
+ ### CEF (Common Event Format)
101
+
102
+ `toCEF(entry)` converts a Loki audit entry into a single-line CEF record.
103
+ Header pipes/backslashes and extension `=`/newlines are escaped per the CEF
104
+ spec, so events cannot break the record framing. Failed events
105
+ (`success: false`) are elevated to severity 8.
106
+
107
+ ```
108
+ CEF:0|Autonomi|Loki Mode|7.49.0|revoke_token|revoke_token|8|rt=2026-02-15T14:30:00.000Z suser=alice src=10.0.0.4 cs1=token cs1Label=resourceType outcome=failure msg=expired credential loki.provider=claude loki.cost=4.25
109
+ ```
110
+
111
+ Nested `details` are flattened under the `loki.` namespace. Standard CEF keys
112
+ are used where they exist (`rt`, `suser`, `src`, `outcome`, `msg`, `cs1..cs3`).
113
+
114
+ ### Splunk HEC JSON
115
+
116
+ `toHEC(entry)` wraps an audit entry in a Splunk HEC envelope (epoch-seconds
117
+ `time`, `sourcetype`, optional `index`, and the raw `event`). `HECSender.send()`
118
+ POSTs it fire-and-forget; network errors are logged, never thrown, so
119
+ observability never breaks the run.
120
+
121
+ ```bash
122
+ export LOKI_SPLUNK_HEC_URL=https://splunk.example.com:8088/services/collector
123
+ export LOKI_SPLUNK_HEC_TOKEN=your-hec-token
124
+ export LOKI_SPLUNK_HEC_INDEX=security
125
+ ```
126
+
127
+ ### GitHub Enterprise SAML SSO (docs-only follow-up)
128
+
129
+ Ingesting GitHub Enterprise SAML/SSO sign-in events is a documented follow-up,
130
+ not a shipped code path. Those events live in the GitHub audit log API and
131
+ require an org-scoped admin token plus an outbound polling client, which is out
132
+ of scope for the local audit path. Recommended approach today: pull the GitHub
133
+ Enterprise audit log via its native streaming to your SIEM (Splunk/Datadog/
134
+ Azure Event Hubs) and correlate on `actor`/`user_id` with Loki Mode events.
135
+
75
136
  ## Splunk Integration
76
137
 
77
138
  ### Method 1: Splunk Universal Forwarder
@@ -262,6 +323,47 @@ export LOKI_SYSLOG_FORMAT=cef
262
323
  # rt=2026-02-15T14:30:00Z suser=user cs1=claude cs1Label=Provider
263
324
  ```
264
325
 
326
+ ## OTEL Vendor Templates (Datadog, Honeycomb)
327
+
328
+ Loki Mode already emits OpenTelemetry traces/metrics when `LOKI_OTEL_ENDPOINT`
329
+ is set (see `src/observability/otel.js`). Ready-to-use vendor templates live in
330
+ `src/observability/siem-export.js` (`OTEL_TEMPLATES`) and produce the exact set
331
+ of env vars to ship to a vendor. They are recipes, not egress: copy the output
332
+ into your shell.
333
+
334
+ ### Datadog
335
+
336
+ Datadog ingests OTLP/HTTP via the Datadog Agent's OTLP receiver (default
337
+ `:4318`) or, agentless, via the OpenTelemetry Collector contrib exporter.
338
+
339
+ ```bash
340
+ # Local Datadog Agent OTLP receiver (recommended)
341
+ export LOKI_OTEL_ENDPOINT=http://localhost:4318
342
+ export LOKI_SERVICE_NAME=loki-mode
343
+
344
+ # Agentless intake (set API key + site)
345
+ export LOKI_OTEL_ENDPOINT=http://localhost:4318
346
+ export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
347
+ export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,dd.site=datadoghq.com"
348
+ ```
349
+
350
+ `site` examples: `datadoghq.com`, `datadoghq.eu`, `us5.datadoghq.com`.
351
+
352
+ ### Honeycomb
353
+
354
+ Honeycomb ingests OTLP/HTTP directly. Auth is the `x-honeycomb-team` header.
355
+
356
+ ```bash
357
+ export LOKI_OTEL_ENDPOINT=https://api.honeycomb.io # or https://api.eu1.honeycomb.io
358
+ export LOKI_SERVICE_NAME=loki-mode
359
+ export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=YOUR_API_KEY,x-honeycomb-dataset=loki"
360
+ ```
361
+
362
+ Note: `OTEL_EXPORTER_OTLP_HEADERS` is honored when the real `@opentelemetry`
363
+ SDK is installed (otel.js prefers it and falls back to the built-in JSON
364
+ exporter). For Datadog the local-agent path holds the API key, so the header is
365
+ optional there.
366
+
265
367
  ## Datadog Security Monitoring
266
368
 
267
369
  ### Log Collection