loki-mode 8.0.3 → 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/README.md +54 -10
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +45 -4
- package/autonomy/council-v2.sh +114 -16
- package/autonomy/grill.sh +32 -0
- package/autonomy/lib/done-recognition.sh +16 -4
- package/autonomy/lib/fast_verify.py +346 -0
- package/autonomy/lib/no_mock_scan.py +21 -1
- package/autonomy/lib/prd-enrich.sh +12 -1
- package/autonomy/lib/proof-generator.py +173 -6
- package/autonomy/lib/proof-template.html +77 -12
- package/autonomy/lib/proof-verify.py +52 -0
- package/autonomy/loki +246 -7
- package/autonomy/run.sh +14 -0
- package/autonomy/verify.sh +12 -6
- package/dashboard/__init__.py +1 -1
- package/events/emit.sh +105 -18
- package/loki-ts/dist/loki.js +319 -316
- package/mcp/__init__.py +30 -12
- package/mcp/server.py +150 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +72 -0
- package/providers/codex.sh +13 -0
- package/providers/loader.sh +10 -4
- package/providers/model_catalog.json +114 -17
- package/providers/models.sh +26 -3
- package/providers/opencode.sh +145 -0
- package/references/design-archetypes.md +85 -0
|
@@ -640,6 +640,23 @@
|
|
|
640
640
|
? " (" + esc(tOutcome) + ")" : "") +
|
|
641
641
|
"</div>";
|
|
642
642
|
}
|
|
643
|
+
// Name the GATE that stopped the run, with how close it came to its
|
|
644
|
+
// threshold. "intervention" tells a reader that something blocked them;
|
|
645
|
+
// "code_review (failed 3 times, threshold 3)" tells them what to fix. The
|
|
646
|
+
// engine already surfaced this in COMPLETION.txt and PAUSED.md -- the
|
|
647
|
+
// signed receipt was the only surface that stayed silent about it.
|
|
648
|
+
// Display-only, and silent when no gate escalation was recorded.
|
|
649
|
+
var tGate = String(g(p, "facts.execution.blocking_gate", "") || "").trim();
|
|
650
|
+
if (tGate) {
|
|
651
|
+
var gFail = g(p, "facts.execution.blocking_gate_failures", null);
|
|
652
|
+
var gThr = g(p, "facts.execution.blocking_gate_threshold", null);
|
|
653
|
+
var detail = "";
|
|
654
|
+
if (typeof gFail === "number" && typeof gThr === "number") {
|
|
655
|
+
detail = " (failed " + esc(String(gFail)) + " times, threshold " +
|
|
656
|
+
esc(String(gThr)) + ")";
|
|
657
|
+
}
|
|
658
|
+
term += '<div class="h-sub">Blocked by: ' + esc(tGate) + detail + "</div>";
|
|
659
|
+
}
|
|
643
660
|
el.className = "honesty " + v.cls;
|
|
644
661
|
el.innerHTML =
|
|
645
662
|
'<span class="h-mark" aria-hidden="true">' + v.mark + "</span>" +
|
|
@@ -1100,6 +1117,24 @@
|
|
|
1100
1117
|
card.innerHTML = (head ? '<div style="margin-bottom:14px;">' + head + "</div>" : "") + body + link;
|
|
1101
1118
|
}
|
|
1102
1119
|
|
|
1120
|
+
// Render one row of gate chips. Shared by the exogenous and advisory blocks.
|
|
1121
|
+
function gateChips(gates) {
|
|
1122
|
+
var chips = "";
|
|
1123
|
+
if (!Array.isArray(gates)) return "";
|
|
1124
|
+
for (var i = 0; i < gates.length; i++) {
|
|
1125
|
+
var st = String(g(gates[i], "status", "")).toLowerCase();
|
|
1126
|
+
var cls = (st === "pass" || st === "passed") ? "pass" : (st === "skip" || st === "skipped") ? "skip" : "fail";
|
|
1127
|
+
var mark = cls === "pass" ? "OK" : cls === "skip" ? "-" : "X";
|
|
1128
|
+
chips += '<span class="gate ' + cls + '"><span class="mk">' + mark + "</span>" +
|
|
1129
|
+
esc(g(gates[i], "name", "gate")) + "</span>";
|
|
1130
|
+
}
|
|
1131
|
+
return chips ? '<div class="gates">' + chips + "</div>" : "";
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// TRUST-4: quality gates split by PROVENANCE, not by outcome. The two blocks
|
|
1135
|
+
// read from quality_gates.exogenous / quality_gates.advisory, the exact field
|
|
1136
|
+
// names proof-generator.py writes. Falls back to the flat pre-split list so an
|
|
1137
|
+
// older receipt still renders.
|
|
1103
1138
|
function renderGates(p) {
|
|
1104
1139
|
var sec = document.getElementById("secGates");
|
|
1105
1140
|
var card = document.getElementById("gatesCard");
|
|
@@ -1110,18 +1145,34 @@
|
|
|
1110
1145
|
sec.classList.add("hide");
|
|
1111
1146
|
return;
|
|
1112
1147
|
}
|
|
1113
|
-
var
|
|
1114
|
-
var
|
|
1115
|
-
if (
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
chips += '<span class="gate ' + cls + '"><span class="mk">' + mark + "</span>" +
|
|
1121
|
-
esc(g(gates[i], "name", "gate")) + "</span>";
|
|
1122
|
-
}
|
|
1148
|
+
var exo = g(p, "quality_gates.exogenous", null);
|
|
1149
|
+
var adv = g(p, "quality_gates.advisory", null);
|
|
1150
|
+
if (!exo && !adv) {
|
|
1151
|
+
// Pre-TRUST-4 receipt: render the flat list exactly as before.
|
|
1152
|
+
card.innerHTML = '<p class="note" style="margin:0 0 12px;">' + passed +
|
|
1153
|
+
" of " + total + " gates passed.</p>" + gateChips(gates);
|
|
1154
|
+
return;
|
|
1123
1155
|
}
|
|
1124
|
-
|
|
1156
|
+
var html = "";
|
|
1157
|
+
var eg = g(exo, "gates", []);
|
|
1158
|
+
if (Array.isArray(eg) && eg.length > 0) {
|
|
1159
|
+
html += '<h3 style="margin:0 0 4px;font-size:14px;">Independent checks (count toward the verdict)</h3>' +
|
|
1160
|
+
'<p class="note" style="margin:0 0 10px;">' +
|
|
1161
|
+
num(g(exo, "passed", 0)) + " of " + num(g(exo, "total", 0)) +
|
|
1162
|
+
" passed. These are deterministic programs the agent cannot write the " +
|
|
1163
|
+
"result of, so only these set the verdict above.</p>" + gateChips(eg);
|
|
1164
|
+
}
|
|
1165
|
+
var ag = g(adv, "gates", []);
|
|
1166
|
+
if (Array.isArray(ag) && ag.length > 0) {
|
|
1167
|
+
html += '<h3 style="margin:18px 0 4px;font-size:14px;">Advisory checks (reported, not counted)</h3>' +
|
|
1168
|
+
'<p class="opinion-note"><span aria-hidden="true">!</span>' +
|
|
1169
|
+
num(g(adv, "passed", 0)) + " of " + num(g(adv, "total", 0)) +
|
|
1170
|
+
" passed. These are the agent judging its own work (it wrote both the " +
|
|
1171
|
+
"tests and the code, or a model graded the result), so a model that is " +
|
|
1172
|
+
"confidently wrong scores itself green here. They are shown for context " +
|
|
1173
|
+
"and can never raise or lower the verdict.</p>" + gateChips(ag);
|
|
1174
|
+
}
|
|
1175
|
+
card.innerHTML = html;
|
|
1125
1176
|
}
|
|
1126
1177
|
|
|
1127
1178
|
function renderFlagged(p) {
|
|
@@ -1187,9 +1238,23 @@
|
|
|
1187
1238
|
if (hash) {
|
|
1188
1239
|
kv += '<div class="k">Integrity hash</div><div class="v">' + esc(algo) + ":" + esc(hash) + "</div>";
|
|
1189
1240
|
}
|
|
1241
|
+
// Signing state, named explicitly. The hash-note below already says the
|
|
1242
|
+
// hash does not prove provenance; without this row a reader is told what
|
|
1243
|
+
// the receipt CANNOT do but never which state this particular receipt is
|
|
1244
|
+
// in, nor how to change it. Signing is gated on LOKI_PROOF_GPG_KEY in
|
|
1245
|
+
// proof-generator.py and is OFF by default -- deliberately, because a
|
|
1246
|
+
// self-minted key would flip proof-verify.py's generator_trusted to false
|
|
1247
|
+
// while the key came from the very process under audit.
|
|
1248
|
+
var signed = !!g(p, "verification.gpg_signature", "");
|
|
1249
|
+
kv += '<div class="k">Signature</div><div class="v">' +
|
|
1250
|
+
(signed ? "SIGNED (detached GPG)" : "UNSIGNED") + "</div>";
|
|
1251
|
+
var signNote = signed
|
|
1252
|
+
? '<p class="hash-note">This receipt carries a detached GPG signature over the same canonical bytes that were hashed. A verifier holding the signer public key can confirm its provenance offline with <code>loki proof verify</code>.</p>'
|
|
1253
|
+
: '<p class="hash-note">This receipt is UNSIGNED, so it trusts the generator that produced it (<code>loki proof verify</code> reports <code>generator_trusted: true</code>). To sign future receipts, set <code>LOKI_PROOF_GPG_KEY</code> to a GPG key id before the run.</p>';
|
|
1190
1254
|
card.innerHTML = '<div class="kv">' + kv + "</div>" +
|
|
1191
1255
|
(hash ? '<p class="hash-note">The integrity hash is a ' + esc(algo) +
|
|
1192
|
-
" digest of this proof. It proves the artifact has not been altered after it was emitted. It does not, by itself, prove who or what produced the work.</p>" : "")
|
|
1256
|
+
" digest of this proof. It proves the artifact has not been altered after it was emitted. It does not, by itself, prove who or what produced the work.</p>" : "") +
|
|
1257
|
+
signNote;
|
|
1193
1258
|
}
|
|
1194
1259
|
|
|
1195
1260
|
function renderCta(p) {
|
|
@@ -65,6 +65,7 @@ CLI:
|
|
|
65
65
|
import hashlib
|
|
66
66
|
import json
|
|
67
67
|
import os
|
|
68
|
+
import re
|
|
68
69
|
import subprocess
|
|
69
70
|
import sys
|
|
70
71
|
|
|
@@ -176,6 +177,57 @@ def _to_int(v, default=0):
|
|
|
176
177
|
# headline re-derivation (MUST match proof-generator._compute_headline exactly)
|
|
177
178
|
# ---------------------------------------------------------------------------
|
|
178
179
|
|
|
180
|
+
# DRIFT-GUARD: mirrored from proof-generator.py. A gate the AGENT authored the
|
|
181
|
+
# input to (it writes both the test and the fix) or that is an LLM judgment
|
|
182
|
+
# cannot be independent evidence, so it must never force or lift a headline.
|
|
183
|
+
# Keyed on the ADVISORY set so an unknown or renamed gate defaults to EXOGENOUS
|
|
184
|
+
# and keeps its power to block -- fail-closed, never fail-open.
|
|
185
|
+
#
|
|
186
|
+
# This block exists because the generator gained a provenance split while this
|
|
187
|
+
# file did not, and the two then disagreed about what a receipt meant. The
|
|
188
|
+
# verifier is the INDEPENDENT re-derivation users are told to trust; if it
|
|
189
|
+
# cannot reproduce the generator's headline, `loki proof verify` reports drift
|
|
190
|
+
# on a receipt that was never tampered with -- a false alarm from our own trust
|
|
191
|
+
# artifact, which is worse than no verifier at all.
|
|
192
|
+
_ADVISORY_GATES = frozenset((
|
|
193
|
+
"test_coverage", "unit_tests", "test_suite", "semantic_tests", "tests",
|
|
194
|
+
"code_review", "devils_advocate", "devil_advocate", "magic_debate",
|
|
195
|
+
"council", "anti_sycophancy",
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _gate_key(name):
|
|
200
|
+
"""Normalize a gate name for provenance lookup (mirrors the generator).
|
|
201
|
+
|
|
202
|
+
run.sh emits the same gate under multiple spellings (`static-analysis` vs
|
|
203
|
+
`static_analysis`) and track_gate_failure appends `_PAUSED`/`_ESCALATED`,
|
|
204
|
+
so an exact-match lookup would misfile real gates.
|
|
205
|
+
"""
|
|
206
|
+
s = str(name or "").strip().lower().replace("-", "_")
|
|
207
|
+
return re.sub(r"_(paused|escalated|not_run|blocked)$", "", s)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _gate_provenance(name):
|
|
211
|
+
"""'advisory' for a model-authored gate, else 'exogenous' (fail-closed)."""
|
|
212
|
+
return "advisory" if _gate_key(name) in _ADVISORY_GATES else "exogenous"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _is_exogenous(gate):
|
|
216
|
+
"""Provenance of a gate dict, honoring a stamped value when present.
|
|
217
|
+
|
|
218
|
+
The generator stamps `provenance` so its `unresolved` override survives (a
|
|
219
|
+
gate that HALTED the run is an execution fact, not a model opinion). We
|
|
220
|
+
honor that stamp and fall back to name lookup for older receipts written
|
|
221
|
+
before the split existed.
|
|
222
|
+
"""
|
|
223
|
+
if not isinstance(gate, dict):
|
|
224
|
+
return True
|
|
225
|
+
stamped = gate.get("provenance")
|
|
226
|
+
if stamped:
|
|
227
|
+
return stamped == "exogenous"
|
|
228
|
+
return _gate_provenance(gate.get("name")) == "exogenous"
|
|
229
|
+
|
|
230
|
+
|
|
179
231
|
def _compute_headline(facts, degraded):
|
|
180
232
|
"""Deterministic headline re-derived from the recorded facts.
|
|
181
233
|
|
package/autonomy/loki
CHANGED
|
@@ -954,10 +954,20 @@ show_help() {
|
|
|
954
954
|
echo " # Providers + model routing"
|
|
955
955
|
echo " loki provider list # Show 4 providers (claude/codex/cline/aider)"
|
|
956
956
|
echo " loki provider set codex # Switch active provider"
|
|
957
|
-
echo " #
|
|
958
|
-
echo "
|
|
959
|
-
echo "
|
|
960
|
-
echo " loki start ./prd.md #
|
|
957
|
+
echo " # Cheap / open models (DeepSeek, GLM, MiniMax, Kimi) via OpenRouter:"
|
|
958
|
+
echo " loki provider set aider # aider + cline reach any OpenRouter model"
|
|
959
|
+
echo " export OPENROUTER_API_KEY=sk-or-..."
|
|
960
|
+
echo " loki start ./prd.md # defaults to deepseek-v3.2 (open weights)"
|
|
961
|
+
echo " # Pick a specific model:"
|
|
962
|
+
echo " export LOKI_AIDER_MODEL=openrouter/z-ai/glm-4.6"
|
|
963
|
+
echo ""
|
|
964
|
+
echo " # Anthropic-PROTOCOL gateways (LiteLLM, Bedrock proxies, self-hosted):"
|
|
965
|
+
echo " export ANTHROPIC_BASE_URL=https://your-gateway/v1 \\\\"
|
|
966
|
+
echo " LOKI_MODEL_OVERRIDE=<model-id-your-gateway-serves>"
|
|
967
|
+
echo " # NOTE: this routes Claude Code itself, so the endpoint must speak the"
|
|
968
|
+
echo " # Anthropic /v1/messages API. OpenRouter does NOT -- it serves"
|
|
969
|
+
echo " # only the OpenAI-shaped /v1/chat/completions. Use the aider or"
|
|
970
|
+
echo " # cline route above for OpenRouter."
|
|
961
971
|
echo ""
|
|
962
972
|
echo " # Cross-project context (Phase F v7.5.23+)"
|
|
963
973
|
echo " # Drop .loki/app.json with {schema_version:1,app_id:myapp,members:[ui,api]}"
|
|
@@ -3871,7 +3881,10 @@ cmd_why() {
|
|
|
3871
3881
|
fi
|
|
3872
3882
|
|
|
3873
3883
|
if [ "$as_json" = "1" ]; then
|
|
3884
|
+
local _why_json_head_sha
|
|
3885
|
+
_why_json_head_sha="$(git rev-parse HEAD 2>/dev/null || echo "")"
|
|
3874
3886
|
_LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
|
|
3887
|
+
_LOKI_WHY_HEAD_SHA="$_why_json_head_sha" \
|
|
3875
3888
|
_LOKI_WHY_LAST_ERROR="$loki_dir/state/LAST_ERROR.json" python3 - <<'WHYJSON'
|
|
3876
3889
|
import json, os
|
|
3877
3890
|
def load(p):
|
|
@@ -3881,7 +3894,26 @@ def load(p):
|
|
|
3881
3894
|
state = load(os.environ.get("_LOKI_WHY_STATE", ""))
|
|
3882
3895
|
comp = load(os.environ.get("_LOKI_WHY_COMPLETION", ""))
|
|
3883
3896
|
last_error = load(os.environ.get("_LOKI_WHY_LAST_ERROR", ""))
|
|
3884
|
-
|
|
3897
|
+
|
|
3898
|
+
# The human report labels a stale completion record "(from previous completed
|
|
3899
|
+
# run)". The JSON consumer got the same record with NO such marker, so a script
|
|
3900
|
+
# reading `completion.branch` or `completion.pr_url` after a crashed run would
|
|
3901
|
+
# attribute a PREVIOUS run's branch and PR to the current one -- silently, and
|
|
3902
|
+
# with no field to check. Emit the identical determination as explicit fields.
|
|
3903
|
+
# Kept byte-identical in logic to the human path below; the two must not drift.
|
|
3904
|
+
live_statuses = {"running", "exited"}
|
|
3905
|
+
head_sha = os.environ.get("_LOKI_WHY_HEAD_SHA", "")
|
|
3906
|
+
comp_head = comp.get("head_sha", "")
|
|
3907
|
+
comp_is_stale = bool(state.get("status") in live_statuses) or bool(
|
|
3908
|
+
head_sha and comp_head and comp_head != head_sha
|
|
3909
|
+
)
|
|
3910
|
+
print(json.dumps({
|
|
3911
|
+
"state": state,
|
|
3912
|
+
"completion": comp,
|
|
3913
|
+
"last_error": last_error,
|
|
3914
|
+
"completion_is_stale": comp_is_stale,
|
|
3915
|
+
"head_sha": head_sha or None,
|
|
3916
|
+
}, indent=2))
|
|
3885
3917
|
WHYJSON
|
|
3886
3918
|
return 0
|
|
3887
3919
|
fi
|
|
@@ -10561,8 +10593,133 @@ EOF
|
|
|
10561
10593
|
}
|
|
10562
10594
|
|
|
10563
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
|
+
|
|
10564
10720
|
cmd_doctor() {
|
|
10565
10721
|
local json_output=false
|
|
10722
|
+
local airgap_audit=false
|
|
10566
10723
|
|
|
10567
10724
|
while [[ $# -gt 0 ]]; do
|
|
10568
10725
|
case "$1" in
|
|
@@ -10570,13 +10727,21 @@ cmd_doctor() {
|
|
|
10570
10727
|
json_output=true
|
|
10571
10728
|
shift
|
|
10572
10729
|
;;
|
|
10730
|
+
--airgap)
|
|
10731
|
+
airgap_audit=true
|
|
10732
|
+
shift
|
|
10733
|
+
;;
|
|
10573
10734
|
--help|-h)
|
|
10574
10735
|
echo -e "${BOLD}loki doctor${NC} - Check system prerequisites"
|
|
10575
10736
|
echo ""
|
|
10576
|
-
echo "Usage: loki doctor [--json]"
|
|
10737
|
+
echo "Usage: loki doctor [--json] [--airgap]"
|
|
10577
10738
|
echo ""
|
|
10578
10739
|
echo "Options:"
|
|
10579
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."
|
|
10580
10745
|
echo ""
|
|
10581
10746
|
echo "Checks: node, python3, jq, git, curl, bash version,"
|
|
10582
10747
|
echo " claude/codex CLIs, disk space, and cockpit render capability."
|
|
@@ -10584,12 +10749,17 @@ cmd_doctor() {
|
|
|
10584
10749
|
;;
|
|
10585
10750
|
*)
|
|
10586
10751
|
echo -e "${RED}Unknown option: $1${NC}"
|
|
10587
|
-
echo "Usage: loki doctor [--json]"
|
|
10752
|
+
echo "Usage: loki doctor [--json] [--airgap]"
|
|
10588
10753
|
return 1
|
|
10589
10754
|
;;
|
|
10590
10755
|
esac
|
|
10591
10756
|
done
|
|
10592
10757
|
|
|
10758
|
+
if [ "$airgap_audit" = true ]; then
|
|
10759
|
+
_loki_airgap_audit "$json_output"
|
|
10760
|
+
return $?
|
|
10761
|
+
fi
|
|
10762
|
+
|
|
10593
10763
|
if [ "$json_output" = true ]; then
|
|
10594
10764
|
cmd_doctor_json
|
|
10595
10765
|
return $?
|
|
@@ -10975,6 +11145,24 @@ except Exception:
|
|
|
10975
11145
|
echo -e " ${YELLOW}WARN${NC} sentrux - not installed (optional, brew install sentrux/tap/sentrux)"
|
|
10976
11146
|
warn_count=$((warn_count + 1))
|
|
10977
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
|
|
10978
11166
|
echo ""
|
|
10979
11167
|
|
|
10980
11168
|
echo -e "${CYAN}System:${NC}"
|
|
@@ -11210,6 +11398,23 @@ sentrux = {
|
|
|
11210
11398
|
'required': 'optional'
|
|
11211
11399
|
}
|
|
11212
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
|
+
|
|
11213
11418
|
# v7.7.17: memory subsystem health surface. Reports the latest entries
|
|
11214
11419
|
# from .loki/memory/.errors.log (rotated by memory/error_log.py) so
|
|
11215
11420
|
# developers see regressions in the previously-silent-fail call sites
|
|
@@ -11263,6 +11468,7 @@ result = {
|
|
|
11263
11468
|
'status': disk_status
|
|
11264
11469
|
},
|
|
11265
11470
|
'sentrux': sentrux,
|
|
11471
|
+
'receipt_signing': receipt_signing,
|
|
11266
11472
|
'memory': memory,
|
|
11267
11473
|
'summary': {
|
|
11268
11474
|
'passed': pass_count,
|
|
@@ -14853,6 +15059,33 @@ with open(manifest_path, 'w') as f:
|
|
|
14853
15059
|
# usable.
|
|
14854
15060
|
# ---------------------------------------------------------------------------
|
|
14855
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
|
+
|
|
14856
15089
|
local verify_mod="$_LOKI_SCRIPT_DIR/verify.sh"
|
|
14857
15090
|
if [ ! -f "$verify_mod" ]; then
|
|
14858
15091
|
echo -e "${RED}Error: verify module not found at $verify_mod${NC}" >&2
|
|
@@ -32282,6 +32515,12 @@ cmd_proof() {
|
|
|
32282
32515
|
echo " --hosted Publish to LOKI_HOSTED_ENDPOINT (open-core seam; no official backend yet)"
|
|
32283
32516
|
echo ""
|
|
32284
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."
|
|
32285
32524
|
[ "$sub" = "" ] && exit 1
|
|
32286
32525
|
exit 0
|
|
32287
32526
|
;;
|
package/autonomy/run.sh
CHANGED
|
@@ -22823,6 +22823,8 @@ check_human_intervention() {
|
|
|
22823
22823
|
log_warn "PAUSE file created by budget limit - NOT auto-clearing in perpetual mode"
|
|
22824
22824
|
log_warn "Budget limit reached. Remove .loki/signals/BUDGET_EXCEEDED and .loki/PAUSE to continue."
|
|
22825
22825
|
notify_intervention_needed "Budget limit reached - execution paused" 2>/dev/null || true
|
|
22826
|
+
# Same-instant snapshot as COMPLETION.txt (see the PAUSE-file site).
|
|
22827
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22826
22828
|
# Genuinely blocking pause: write the durable intervention record
|
|
22827
22829
|
# now (state-only; the ping above already fired). This is the
|
|
22828
22830
|
# correct site for the durable file because the run actually halts
|
|
@@ -22835,6 +22837,7 @@ check_human_intervention() {
|
|
|
22835
22837
|
if [ "$pause_result" -eq 1 ]; then
|
|
22836
22838
|
# STOP requested DURING the pause: relabel the durable record
|
|
22837
22839
|
# as stopped (state-only; the user typed STOP and is aware).
|
|
22840
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22838
22841
|
build_completion_summary stopped 2>/dev/null || true
|
|
22839
22842
|
return 2
|
|
22840
22843
|
fi
|
|
@@ -22849,6 +22852,13 @@ check_human_intervention() {
|
|
|
22849
22852
|
fi
|
|
22850
22853
|
log_warn "PAUSE file detected - pausing execution"
|
|
22851
22854
|
notify_intervention_needed "Execution paused via PAUSE file"
|
|
22855
|
+
# Refresh STATUS.txt from the queue BEFORE writing the durable record, so
|
|
22856
|
+
# the two files snapshot the same instant. The status monitor refreshes
|
|
22857
|
+
# every 2s but is not running by the time we block here, so STATUS.txt
|
|
22858
|
+
# was frozen at its last tick: measured on a real paused run reporting
|
|
22859
|
+
# "Failed: 0" while COMPLETION.txt -- written now, from the same
|
|
22860
|
+
# queue/failed.json -- reported failed=1. Same source, different age.
|
|
22861
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22852
22862
|
# Genuinely blocking pause: write the durable intervention record now
|
|
22853
22863
|
# (state-only; the ping above already fired).
|
|
22854
22864
|
build_completion_summary intervention 2>/dev/null || true
|
|
@@ -22859,6 +22869,7 @@ check_human_intervention() {
|
|
|
22859
22869
|
if [ "$pause_result" -eq 1 ]; then
|
|
22860
22870
|
# STOP was requested during pause: relabel the durable record as
|
|
22861
22871
|
# stopped (state-only; the user typed STOP and is aware).
|
|
22872
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22862
22873
|
build_completion_summary stopped 2>/dev/null || true
|
|
22863
22874
|
return 2
|
|
22864
22875
|
fi
|
|
@@ -22872,6 +22883,8 @@ check_human_intervention() {
|
|
|
22872
22883
|
rm -f "$loki_dir/PAUSE_AT_CHECKPOINT"
|
|
22873
22884
|
notify_intervention_needed "Execution paused at checkpoint"
|
|
22874
22885
|
touch "$loki_dir/PAUSE"
|
|
22886
|
+
# Same-instant snapshot as COMPLETION.txt (see the PAUSE-file site).
|
|
22887
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22875
22888
|
# Genuinely blocking pause: write the durable intervention record now
|
|
22876
22889
|
# (state-only; the ping above already fired).
|
|
22877
22890
|
build_completion_summary intervention 2>/dev/null || true
|
|
@@ -22881,6 +22894,7 @@ check_human_intervention() {
|
|
|
22881
22894
|
rm -f "$loki_dir/PAUSE"
|
|
22882
22895
|
if [ "$pause_result" -eq 1 ]; then
|
|
22883
22896
|
# STOP requested during pause: relabel as stopped (state-only).
|
|
22897
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22884
22898
|
build_completion_summary stopped 2>/dev/null || true
|
|
22885
22899
|
return 2
|
|
22886
22900
|
fi
|
package/autonomy/verify.sh
CHANGED
|
@@ -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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|