loki-mode 8.1.0 → 8.2.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.
package/autonomy/loki CHANGED
@@ -10593,8 +10593,133 @@ EOF
10593
10593
  }
10594
10594
 
10595
10595
  # Check system prerequisites
10596
+ # _loki_airgap_audit <json?> -- enumerate every network egress this engine can
10597
+ # perform, classify each as REQUIRED or optional, and say how to disable it.
10598
+ #
10599
+ # WHY THIS EXISTS
10600
+ # Air-gapped deployment is a real enterprise requirement and the incumbent
10601
+ # (Tabnine) states it can "be deployed in a completely air-gapped environment"
10602
+ # without publishing WHICH hosts it would otherwise contact. A claim you
10603
+ # cannot audit is a claim you have to take on faith.
10604
+ #
10605
+ # Sourcegraph is the honest counter-example: their own whitepaper states that
10606
+ # self-hosted Cody still "sends requests from the Sourcegraph instance to
10607
+ # Anthropic's API". Self-hosted control plane, egressing inference.
10608
+ #
10609
+ # So the credible position is not "trust us, we are air-gapped" -- it is here
10610
+ # is every host, here is which ones are required, here is how to turn each one
10611
+ # off, now verify it yourself with tcpdump. We would rather be auditable than
10612
+ # assertive.
10613
+ #
10614
+ # Exit 0 when no REQUIRED egress remains (air-gap achievable in this config),
10615
+ # 1 otherwise. Deterministic: reads configuration, performs NO network calls.
10616
+ _loki_airgap_audit() {
10617
+ local as_json="${1:-false}"
10618
+
10619
+ # Provider inference endpoint: the one egress that is genuinely required,
10620
+ # unless the active provider serves weights locally.
10621
+ local provider="${LOKI_PROVIDER:-claude}"
10622
+ local model_host="" model_required="yes" model_note=""
10623
+ case "$provider" in
10624
+ claude)
10625
+ model_host="${ANTHROPIC_BASE_URL:-https://api.anthropic.com}"
10626
+ model_note="Claude Code inference. Set ANTHROPIC_BASE_URL to an in-network gateway, or switch to a local-weights provider."
10627
+ ;;
10628
+ codex)
10629
+ model_host="${OPENAI_BASE_URL:-https://api.openai.com}"
10630
+ model_note="Codex inference. Point at an in-network gateway or switch provider."
10631
+ ;;
10632
+ opencode|cline|aider)
10633
+ model_host="${OPENAI_BASE_URL:-${OPENROUTER_API_KEY:+https://openrouter.ai}}"
10634
+ if [ -z "$model_host" ] || printf '%s' "${LOKI_OPENCODE_MODEL:-}${LOKI_AIDER_MODEL:-}" | grep -qiE 'ollama|localhost|127\.0\.0\.1|lmstudio'; then
10635
+ model_host="local weights"
10636
+ model_required="no"
10637
+ model_note="Local model runtime (Ollama / LM Studio / llama.cpp). No inference egress."
10638
+ else
10639
+ model_note="Remote model gateway. Use an ollama/ model id for zero-egress inference."
10640
+ fi
10641
+ ;;
10642
+ *)
10643
+ model_host="unknown (provider '$provider')"
10644
+ model_note="Unrecognized provider; audit its config manually."
10645
+ ;;
10646
+ esac
10647
+
10648
+ # Optional egress, each individually disableable.
10649
+ local tel_state="off"
10650
+ [ "${LOKI_TELEMETRY:-}" = "on" ] && tel_state="on"
10651
+ local upd_state="on"
10652
+ [ "${LOKI_NO_UPDATE_CHECK:-0}" = "1" ] && upd_state="off"
10653
+
10654
+ if [ "$as_json" = "true" ]; then
10655
+ _AG_PROV="$provider" _AG_HOST="$model_host" _AG_REQ="$model_required" \
10656
+ _AG_NOTE="$model_note" _AG_TEL="$tel_state" _AG_UPD="$upd_state" \
10657
+ python3 -c '
10658
+ import json, os
10659
+ req = os.environ["_AG_REQ"] == "yes"
10660
+ egress = [{
10661
+ "name": "model_inference",
10662
+ "host": os.environ["_AG_HOST"],
10663
+ "required": req,
10664
+ "disable": "use a local-weights provider (ollama/, lmstudio/) or an in-network gateway",
10665
+ "note": os.environ["_AG_NOTE"],
10666
+ }, {
10667
+ "name": "telemetry",
10668
+ "host": "telemetry endpoint",
10669
+ "required": False,
10670
+ "enabled": os.environ["_AG_TEL"] == "on",
10671
+ "disable": "loki telemetry off (already the default)",
10672
+ }, {
10673
+ "name": "update_check",
10674
+ "host": "registry.npmjs.org",
10675
+ "required": False,
10676
+ "enabled": os.environ["_AG_UPD"] == "on",
10677
+ "disable": "export LOKI_NO_UPDATE_CHECK=1",
10678
+ }]
10679
+ blockers = [e for e in egress if e.get("required")]
10680
+ print(json.dumps({
10681
+ "provider": os.environ["_AG_PROV"],
10682
+ "airgap_ready": not blockers,
10683
+ "required_egress": [e["name"] for e in blockers],
10684
+ "egress": egress,
10685
+ }, indent=2))
10686
+ '
10687
+ [ "$model_required" = "yes" ] && return 1
10688
+ return 0
10689
+ fi
10690
+
10691
+ echo -e "${BOLD}Network egress audit${NC}"
10692
+ echo ""
10693
+ echo " Active provider: $provider"
10694
+ echo ""
10695
+ echo -e "${BOLD}Egress points${NC}"
10696
+ if [ "$model_required" = "yes" ]; then
10697
+ echo -e " ${YELLOW}REQUIRED${NC} model inference -> $model_host"
10698
+ else
10699
+ echo -e " ${GREEN}none${NC} model inference -> $model_host"
10700
+ fi
10701
+ echo " $model_note"
10702
+ echo -e " optional telemetry [$tel_state] disable: loki telemetry off (default off)"
10703
+ echo -e " optional update check [$upd_state] disable: export LOKI_NO_UPDATE_CHECK=1"
10704
+ echo ""
10705
+ if [ "$model_required" = "yes" ]; then
10706
+ echo -e "${YELLOW}Not air-gap ready in this configuration.${NC}"
10707
+ echo " One required egress remains: model inference."
10708
+ echo " To close it, serve the model in-network:"
10709
+ echo " loki provider set opencode"
10710
+ echo " export LOKI_OPENCODE_MODEL=ollama/qwen2.5-coder"
10711
+ echo ""
10712
+ echo " Then re-run: loki doctor --airgap"
10713
+ return 1
10714
+ fi
10715
+ echo -e "${GREEN}Air-gap ready: no required egress in this configuration.${NC}"
10716
+ echo " Verify independently -- run with the interface down, or watch with tcpdump."
10717
+ return 0
10718
+ }
10719
+
10596
10720
  cmd_doctor() {
10597
10721
  local json_output=false
10722
+ local airgap_audit=false
10598
10723
 
10599
10724
  while [[ $# -gt 0 ]]; do
10600
10725
  case "$1" in
@@ -10602,13 +10727,21 @@ cmd_doctor() {
10602
10727
  json_output=true
10603
10728
  shift
10604
10729
  ;;
10730
+ --airgap)
10731
+ airgap_audit=true
10732
+ shift
10733
+ ;;
10605
10734
  --help|-h)
10606
10735
  echo -e "${BOLD}loki doctor${NC} - Check system prerequisites"
10607
10736
  echo ""
10608
- echo "Usage: loki doctor [--json]"
10737
+ echo "Usage: loki doctor [--json] [--airgap]"
10609
10738
  echo ""
10610
10739
  echo "Options:"
10611
10740
  echo " --json Output machine-readable JSON"
10741
+ echo " --airgap Audit network egress: list every host this engine"
10742
+ echo " can contact, whether it is REQUIRED or optional,"
10743
+ echo " and how to disable it. Exits non-zero if any"
10744
+ echo " required egress remains."
10612
10745
  echo ""
10613
10746
  echo "Checks: node, python3, jq, git, curl, bash version,"
10614
10747
  echo " claude/codex CLIs, disk space, and cockpit render capability."
@@ -10616,12 +10749,17 @@ cmd_doctor() {
10616
10749
  ;;
10617
10750
  *)
10618
10751
  echo -e "${RED}Unknown option: $1${NC}"
10619
- echo "Usage: loki doctor [--json]"
10752
+ echo "Usage: loki doctor [--json] [--airgap]"
10620
10753
  return 1
10621
10754
  ;;
10622
10755
  esac
10623
10756
  done
10624
10757
 
10758
+ if [ "$airgap_audit" = true ]; then
10759
+ _loki_airgap_audit "$json_output"
10760
+ return $?
10761
+ fi
10762
+
10625
10763
  if [ "$json_output" = true ]; then
10626
10764
  cmd_doctor_json
10627
10765
  return $?
@@ -11007,6 +11145,24 @@ except Exception:
11007
11145
  echo -e " ${YELLOW}WARN${NC} sentrux - not installed (optional, brew install sentrux/tap/sentrux)"
11008
11146
  warn_count=$((warn_count + 1))
11009
11147
  fi
11148
+ # Evidence Receipt signing state (optional). Unsigned receipts are the
11149
+ # default and are still checkable (hash + diff re-derivation); what they
11150
+ # cannot prove is PROVENANCE, so proof-verify.py reports
11151
+ # generator_trusted: true. Surface the state here because it is otherwise
11152
+ # invisible until someone reads a receipt. WARN (never FAIL): unsigned is
11153
+ # a supported, documented mode, not a broken install.
11154
+ if [ -n "${LOKI_PROOF_GPG_KEY:-}" ]; then
11155
+ if command -v gpg &>/dev/null; then
11156
+ echo -e " ${GREEN}PASS${NC} Receipt signing: LOKI_PROOF_GPG_KEY set and gpg available"
11157
+ pass_count=$((pass_count + 1))
11158
+ else
11159
+ echo -e " ${YELLOW}WARN${NC} Receipt signing: LOKI_PROOF_GPG_KEY set but gpg NOT on PATH - receipts will be UNSIGNED"
11160
+ warn_count=$((warn_count + 1))
11161
+ fi
11162
+ else
11163
+ echo -e " ${YELLOW}WARN${NC} Receipt signing: UNSIGNED (set LOKI_PROOF_GPG_KEY to a gpg key id; see docs/SIGNED-RECEIPTS.md)"
11164
+ warn_count=$((warn_count + 1))
11165
+ fi
11010
11166
  echo ""
11011
11167
 
11012
11168
  echo -e "${CYAN}System:${NC}"
@@ -11242,6 +11398,23 @@ sentrux = {
11242
11398
  'required': 'optional'
11243
11399
  }
11244
11400
 
11401
+ # Evidence Receipt signing state. Unsigned is the DEFAULT and is a supported
11402
+ # mode, so this is never 'fail'. Exposed because signing state is otherwise
11403
+ # invisible until someone reads a receipt, and it is exactly what decides
11404
+ # whether proof-verify.py reports generator_trusted: true. Sibling of
11405
+ # checks/disk/sentrux -- deliberately NOT counted in the summary tally so
11406
+ # existing consumers see unchanged numbers.
11407
+ signing_key_set = bool(os.environ.get('LOKI_PROOF_GPG_KEY', '').strip())
11408
+ signing_gpg_available = shutil.which('gpg') is not None
11409
+ signing_enabled = signing_key_set and signing_gpg_available
11410
+ receipt_signing = {
11411
+ 'enabled': signing_enabled,
11412
+ 'key_configured': signing_key_set,
11413
+ 'gpg_available': signing_gpg_available,
11414
+ 'status': 'pass' if signing_enabled else 'warn',
11415
+ 'required': 'optional'
11416
+ }
11417
+
11245
11418
  # v7.7.17: memory subsystem health surface. Reports the latest entries
11246
11419
  # from .loki/memory/.errors.log (rotated by memory/error_log.py) so
11247
11420
  # developers see regressions in the previously-silent-fail call sites
@@ -11295,6 +11468,7 @@ result = {
11295
11468
  'status': disk_status
11296
11469
  },
11297
11470
  'sentrux': sentrux,
11471
+ 'receipt_signing': receipt_signing,
11298
11472
  'memory': memory,
11299
11473
  'summary': {
11300
11474
  'passed': pass_count,
@@ -14885,6 +15059,33 @@ with open(manifest_path, 'w') as f:
14885
15059
  # usable.
14886
15060
  # ---------------------------------------------------------------------------
14887
15061
  cmd_verify() {
15062
+ # --fast: deterministic checks only, in milliseconds. Measured on this repo
15063
+ # (1,932 tracked files): 298 ms cold, 87 ms warm, 19 ms diff-scoped, against
15064
+ # an 11,040 ms shell-based baseline.
15065
+ #
15066
+ # It runs ONLY exogenous checks -- no model call, no network -- so every
15067
+ # verdict is reproducible by anyone with the same commit. That is the point,
15068
+ # not a limitation: a verdict you can re-derive is a fact, and a fact is what
15069
+ # an IDE, a CI step, or another vendor's agent can safely act on without
15070
+ # trusting our model choices.
15071
+ #
15072
+ # The full `loki verify` remains the deeper, slower path.
15073
+ for _fv_arg in "$@"; do
15074
+ if [ "$_fv_arg" = "--fast" ]; then
15075
+ local _fv_py="$_LOKI_SCRIPT_DIR/lib/fast_verify.py"
15076
+ if [ ! -f "$_fv_py" ]; then
15077
+ echo -e "${RED}Error: fast_verify not found at $_fv_py${NC}" >&2
15078
+ return 3
15079
+ fi
15080
+ local _fv_argv=()
15081
+ for _a in "$@"; do
15082
+ [ "$_a" = "--fast" ] || _fv_argv+=("$_a")
15083
+ done
15084
+ python3 "$_fv_py" "${_fv_argv[@]+"${_fv_argv[@]}"}"
15085
+ return $?
15086
+ fi
15087
+ done
15088
+
14888
15089
  local verify_mod="$_LOKI_SCRIPT_DIR/verify.sh"
14889
15090
  if [ ! -f "$verify_mod" ]; then
14890
15091
  echo -e "${RED}Error: verify module not found at $verify_mod${NC}" >&2
@@ -32314,6 +32515,12 @@ cmd_proof() {
32314
32515
  echo " --hosted Publish to LOKI_HOSTED_ENDPOINT (open-core seam; no official backend yet)"
32315
32516
  echo ""
32316
32517
  echo "Proofs are generated automatically at run completion (LOKI_PROOF=0 to opt out)."
32518
+ echo ""
32519
+ echo "Signing (off by default): receipts are UNSIGNED unless LOKI_PROOF_GPG_KEY is"
32520
+ echo "set to a gpg key id. Unsigned, the integrity hash proves the bytes were not"
32521
+ echo "edited after hashing but NOT who produced them, so verify reports"
32522
+ echo "generator_trusted: true. Export LOKI_PROOF_GPG_KEY to close that gap."
32523
+ echo "See docs/SIGNED-RECEIPTS.md."
32317
32524
  [ "$sub" = "" ] && exit 1
32318
32525
  exit 0
32319
32526
  ;;
@@ -781,13 +781,19 @@ verify_gate_nomock() {
781
781
  return 0
782
782
  fi
783
783
  local status_line
784
+ # The changed-file list goes over STDIN, not an env var. A single env string
785
+ # is capped at MAX_ARG_STRLEN (131071 bytes) on Linux; a large changed set
786
+ # (e.g. a generated source tree) makes execve fail with E2BIG, the fallback
787
+ # below fires, and the gate silently degrades to inconclusive. macOS has no
788
+ # per-string cap, so that failure was Linux-only.
784
789
  status_line=$(
785
- _NM_FILES="$changed" \
786
- _NM_TREE="$tree" \
787
- _NM_OUT="$scan_file" \
788
- _NM_BASE="${VERIFY_MERGE_BASE:-}" \
789
- python3 -I "$scanner" 2>/dev/null \
790
- || echo "INCONCLUSIVE:detector_error"
790
+ printf '%s\n' "$changed" | {
791
+ _NM_TREE="$tree" \
792
+ _NM_OUT="$scan_file" \
793
+ _NM_BASE="${VERIFY_MERGE_BASE:-}" \
794
+ python3 -I "$scanner" 2>/dev/null \
795
+ || echo "INCONCLUSIVE:detector_error"
796
+ }
791
797
  )
792
798
 
793
799
  case "$status_line" in
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.1.0"
10
+ __version__ = "8.2.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
package/events/emit.sh CHANGED
@@ -71,6 +71,26 @@ safe_append_event_jsonl() {
71
71
  local max_attempts=100 # ~1s at 10ms sleep
72
72
  local stale_after=30
73
73
  while ! mkdir "$lock_dir" 2>/dev/null; do
74
+ # COUNT EVERY ITERATION, before any `continue` can skip the increment.
75
+ #
76
+ # This was the v8.1.0 fix's blind spot and it kept the P0 alive. Both
77
+ # `continue` paths below (lock vanished mid-stat; stale lock reclaimed)
78
+ # jumped PAST the increment that used to live at the bottom of the loop,
79
+ # so `max_attempts` was unreachable on those paths and the loop spun
80
+ # forever. Under concurrent emits the stale-reclaim path fires over and
81
+ # over, which is precisely the pathological case.
82
+ #
83
+ # MEASURED 2026-07-30, AFTER the v8.1.0 fix shipped: 63 orphaned
84
+ # emit.sh processes, oldest alive 10h51m, each burning ~5% CPU, machine
85
+ # at load 63 on 14 cores. Every orphan started after the fix landed.
86
+ # An unbounded wait whose only exit is a counter must increment that
87
+ # counter on EVERY path, or the bound is decorative.
88
+ attempts=$((attempts + 1))
89
+ if [ "$attempts" -ge "$max_attempts" ]; then
90
+ # Give up fast -- best-effort write so observability never blocks.
91
+ printf '%s\n' "$line" >> "$events_path" 2>/dev/null || true
92
+ return 0
93
+ fi
74
94
  # Check staleness EVERY iteration, not only after exhausting attempts.
75
95
  # The old code waited for all 500 attempts before its first staleness
76
96
  # check, so a stale lock cost ~5s AND ~500 forked sleep helpers per
@@ -90,12 +110,8 @@ safe_append_event_jsonl() {
90
110
  rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir" 2>/dev/null || true
91
111
  continue
92
112
  fi
93
- attempts=$((attempts + 1))
94
- if [ "$attempts" -ge "$max_attempts" ]; then
95
- # Give up fast -- best-effort write so observability never blocks.
96
- printf '%s\n' "$line" >> "$events_path" 2>/dev/null || true
97
- return 0
98
- fi
113
+ # (attempt counting and the give-up branch moved to the TOP of the loop
114
+ # so no `continue` can bypass them)
99
115
  # Sleep ~10ms WITHOUT forking when the shell supports fractional sleep
100
116
  # (bash's `read -t` needs no external process). perl/sleep are fallbacks.
101
117
  read -r -t 0.01 _unused_ < /dev/null 2>/dev/null \
@@ -117,6 +133,50 @@ fi
117
133
 
118
134
  set -euo pipefail
119
135
 
136
+ #-----------------------------------------------------------------------------
137
+ # SELF-REAPER: telemetry must never outlive the thing it observes.
138
+ #-----------------------------------------------------------------------------
139
+ # WHY A WATCHDOG AND NOT ANOTHER LOCK FIX. v8.1.0 fixed a specific unbounded
140
+ # wait (bare `flock -x`, and a staleness check that ran only after 500 attempts).
141
+ # It was a real fix and it was not sufficient: MEASURED 2026-07-30, AFTER that
142
+ # release, 63 orphaned emit.sh processes were alive on this machine, the oldest
143
+ # 10h51m, each burning ~5% CPU, contributing to load 63 on 14 cores. Every one
144
+ # of them started AFTER the fix landed, so whatever wedges emit.sh is not (only)
145
+ # the path that was fixed.
146
+ #
147
+ # The lesson is that patching each discovered hang is an arms race we keep
148
+ # losing, because a hang anywhere in this script has the same user-visible cost.
149
+ # emit.sh is FIRE-AND-FORGET telemetry: nothing waits on its result, and a
150
+ # dropped event is strictly cheaper than a wedged process. So instead of proving
151
+ # no path can block, we cap the lifetime of EVERY path.
152
+ #
153
+ # A background timer SIGKILLs this process after LOKI_EMIT_MAX_SECONDS (default
154
+ # 10). It is deliberately blunt: no cleanup hook, no graceful drain, because the
155
+ # failure mode being defended against is precisely "graceful paths did not run".
156
+ # The killer is disowned so it never becomes a job the parent shell waits on,
157
+ # and it exits immediately when the main process finishes normally.
158
+ #
159
+ # Set LOKI_EMIT_MAX_SECONDS=0 to disable (useful only when debugging emit.sh
160
+ # itself -- an operator who disables it is choosing the orphan risk knowingly).
161
+ _emit_max="${LOKI_EMIT_MAX_SECONDS:-10}"
162
+ case "$_emit_max" in ''|*[!0-9]*) _emit_max=10 ;; esac
163
+ if [ "$_emit_max" -gt 0 ]; then
164
+ _emit_target=$$
165
+ (
166
+ # Poll rather than one long sleep so the watchdog exits promptly on the
167
+ # normal path instead of lingering for the full window.
168
+ _waited=0
169
+ while [ "$_waited" -lt "$_emit_max" ]; do
170
+ sleep 1
171
+ kill -0 "$_emit_target" 2>/dev/null || exit 0
172
+ _waited=$((_waited + 1))
173
+ done
174
+ kill -9 "$_emit_target" 2>/dev/null || true
175
+ ) >/dev/null 2>&1 &
176
+ # Disown so the watchdog is not a tracked job of the caller's shell.
177
+ disown 2>/dev/null || true
178
+ fi
179
+
120
180
  # Configuration
121
181
  LOKI_DIR="${LOKI_DIR:-.loki}"
122
182
  EVENTS_DIR="$LOKI_DIR/events/pending"