loki-mode 7.109.0 → 7.111.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +5 -2
- package/autonomy/completion-council.sh +8 -1
- package/autonomy/lib/proof-generator.py +10 -4
- package/autonomy/lib/proof-pr.sh +14 -0
- package/autonomy/lib/proof-verify.py +161 -15
- package/autonomy/lib/proof_redact.py +6 -1
- package/autonomy/loki +4 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +59 -6
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/lsp_proxy.py +94 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/aider.sh +2 -2
- package/providers/claude.sh +1 -1
- package/providers/cline.sh +2 -2
- package/providers/codex.sh +13 -5
- package/providers/models.sh +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.111.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
408
408
|
|
|
409
409
|
---
|
|
410
410
|
|
|
411
|
-
**v7.
|
|
411
|
+
**v7.111.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.111.0
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -673,8 +673,11 @@ _detect_port() {
|
|
|
673
673
|
fi
|
|
674
674
|
local port
|
|
675
675
|
# Handle both simple (HOST:CONTAINER) and IP-bound (IP:HOST:CONTAINER) port formats
|
|
676
|
-
# Also handle port ranges like "8080-8090:8080-8090" by taking the
|
|
677
|
-
|
|
676
|
+
# Also handle port ranges like "8080-8090:8080-8090" by taking the FIRST port.
|
|
677
|
+
# The leading strip must be anchored (^...) and must NOT use a greedy `.*-`,
|
|
678
|
+
# otherwise it consumes the range's internal dash and yields the LAST port
|
|
679
|
+
# (e.g. "8080-8090:8080-8090" -> 8090 instead of 8080). See wave-3 repro.
|
|
680
|
+
port=$(grep -E '^\s*-\s*"?[0-9]' "$compose_file" 2>/dev/null | head -1 | sed -E 's/^[[:space:]]*-[[:space:]]*"?//; s/".*$//' | awk -F: '{print $(NF-1)}' | awk -F- '{print $1}')
|
|
678
681
|
_APP_RUNNER_PORT="${port:-8080}"
|
|
679
682
|
fi
|
|
680
683
|
;;
|
|
@@ -3103,7 +3103,14 @@ council_evaluate() {
|
|
|
3103
3103
|
# Step 3: Unanimous -- run devil's advocate
|
|
3104
3104
|
local da_result
|
|
3105
3105
|
da_result=$(council_devils_advocate_review "$ITERATION_COUNT")
|
|
3106
|
-
|
|
3106
|
+
# council_devils_advocate_review emits log_warn/log_info lines to
|
|
3107
|
+
# STDOUT (run.sh log_* echo to stdout, not stderr), so da_result is a
|
|
3108
|
+
# multi-line string whose LAST line is the verdict token. Comparing
|
|
3109
|
+
# the whole capture against "OVERRIDE_CONTINUE" never matched, which
|
|
3110
|
+
# silently defeated the anti-sycophancy backstop. Take the last line.
|
|
3111
|
+
local da_verdict
|
|
3112
|
+
da_verdict="$(printf '%s\n' "$da_result" | tail -n1)"
|
|
3113
|
+
if [ "$da_verdict" = "OVERRIDE_CONTINUE" ]; then
|
|
3107
3114
|
log_warn "Council evaluate: devil's advocate overrode unanimous COMPLETE"
|
|
3108
3115
|
# Write transcript: DA triggered and flipped the outcome (Path B)
|
|
3109
3116
|
council_write_transcript "${ITERATION_COUNT:-0}" "REJECTED" "true" "true" "$_eval_threshold"
|
|
@@ -675,8 +675,11 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
675
675
|
|
|
676
676
|
# ---- v1.1 evidence model -------------------------------------------------
|
|
677
677
|
# FACTS: deterministic, re-derivable, NON-LLM. A skeptic can recompute every
|
|
678
|
-
# one of these from the same .loki state.
|
|
679
|
-
#
|
|
678
|
+
# one of these from the same .loki state. The headline is computed ONLY from
|
|
679
|
+
# these facts. NOTE: this is NOT tamper-proof against a hand-forger on the
|
|
680
|
+
# unsigned path -- whoever can write the proof can also rewrite the facts and
|
|
681
|
+
# recompute the hash. True non-forgeability requires the neutral signed record
|
|
682
|
+
# (service-held key). See proof-verify.py verify() docstring.
|
|
680
683
|
git_facts = {
|
|
681
684
|
"base_sha": os.environ.get("_LOKI_ITER_START_SHA", "").strip(),
|
|
682
685
|
"head_sha": _git_head_sha(target_dir),
|
|
@@ -725,8 +728,11 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
725
728
|
}
|
|
726
729
|
|
|
727
730
|
# HONESTY: every fact that is not_run/inconclusive/skipped, surfaced loudly,
|
|
728
|
-
# plus a deterministic headline
|
|
729
|
-
# exit_code:0 evidence and a non-empty diff.
|
|
731
|
+
# plus a deterministic headline derived from the recorded facts (real
|
|
732
|
+
# exit_code:0 evidence and a non-empty diff). On the unsigned path this
|
|
733
|
+
# deters an inconsistent editor, but does NOT stop a consistent hand-forger
|
|
734
|
+
# who rewrites the facts and re-hashes; neutral non-forgeability needs the
|
|
735
|
+
# signed record.
|
|
730
736
|
degraded = _compute_degraded(facts)
|
|
731
737
|
headline = _compute_headline(facts, degraded)
|
|
732
738
|
honesty = {
|
package/autonomy/lib/proof-pr.sh
CHANGED
|
@@ -235,6 +235,20 @@ def main():
|
|
|
235
235
|
"nothing could be verified. The headline is computed only from "
|
|
236
236
|
"deterministic, re-derivable facts, never from an AI opinion."
|
|
237
237
|
)
|
|
238
|
+
_line()
|
|
239
|
+
# Honest scope of "verify it yourself". A reviewer CAN recompute the diff and
|
|
240
|
+
# the integrity hash, and proof-verify.py re-derives the headline from the
|
|
241
|
+
# recorded facts to catch an inconsistent edit. But on the unsigned path the
|
|
242
|
+
# recorded test/build facts are produced by Loki and taken at face value:
|
|
243
|
+
# a consistent rewrite of facts + headline is not caught here. Neutral,
|
|
244
|
+
# adversarial non-forgeability (Loki not trusted) needs the signed record.
|
|
245
|
+
_line(
|
|
246
|
+
"Scope: you can recompute the diff and the integrity hash yourself, and "
|
|
247
|
+
"`loki proof verify` re-derives the headline from the recorded facts. On "
|
|
248
|
+
"an unsigned receipt the recorded test/build facts are produced by Loki "
|
|
249
|
+
"and taken at face value; a signed receipt is required for verification "
|
|
250
|
+
"that does not trust Loki."
|
|
251
|
+
)
|
|
238
252
|
return 0
|
|
239
253
|
|
|
240
254
|
|
|
@@ -2,13 +2,26 @@
|
|
|
2
2
|
"""Deterministic re-verifier for Loki Mode proof-of-run receipts.
|
|
3
3
|
|
|
4
4
|
Companion to proof-generator.py. The generator writes proof.json with an
|
|
5
|
-
integrity hash so a skeptic can prove the JSON
|
|
6
|
-
|
|
7
|
-
live repo, so a skeptic can prove the
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
integrity hash so a skeptic can prove the JSON bytes were not edited since
|
|
6
|
+
they were hashed. This module goes further: it re-checks the receipt's
|
|
7
|
+
recorded FACTS against the live repo, so a skeptic can prove the recorded
|
|
8
|
+
diff STILL matches what is in git, not merely that the JSON bytes are
|
|
9
|
+
unaltered. It also re-derives the honesty headline from the recorded facts
|
|
10
|
+
(see headline_consistent below) to catch an INCONSISTENT edit -- a proof
|
|
11
|
+
whose headline was flipped to VERIFIED while the facts still say not_run.
|
|
12
|
+
|
|
13
|
+
What this does NOT do -- honest scope. On the UNSIGNED path (no valid gpg
|
|
14
|
+
signature) the generator is TRUSTED: the recorded facts are taken at face
|
|
15
|
+
value. A forger who rewrites BOTH the facts AND the headline to a mutually
|
|
16
|
+
consistent lie, then recomputes the integrity hash, still passes every
|
|
17
|
+
check here -- the hash proves only bytes-unedited-since-hashing, and the
|
|
18
|
+
re-derived diff can be made to match a repo the forger controls. Neutral,
|
|
19
|
+
adversarial non-forgeability (the generator is NOT trusted) requires the
|
|
20
|
+
SIGNED record: a detached gpg signature from a key the verifier trusts.
|
|
21
|
+
When gpg_ok is True the generator is not trusted; when gpg_ok is "n/a" it
|
|
22
|
+
is. This module reports that distinction (generator_trusted) rather than
|
|
23
|
+
overclaiming that re-checking the diff makes a receipt non-forgeable. It
|
|
24
|
+
does not.
|
|
12
25
|
|
|
13
26
|
Three checks (mirrors dashboard/audit.py verify-CLI style):
|
|
14
27
|
|
|
@@ -165,6 +178,61 @@ def _to_int(v, default=0):
|
|
|
165
178
|
return default
|
|
166
179
|
|
|
167
180
|
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# headline re-derivation (MUST match proof-generator._compute_headline exactly)
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
def _compute_headline(facts, degraded):
|
|
186
|
+
"""Deterministic headline re-derived from the recorded facts.
|
|
187
|
+
|
|
188
|
+
MUST match proof-generator.py _compute_headline() byte-for-byte in logic
|
|
189
|
+
(same DRIFT-GUARD contract as _canonical / _diff_sha256_from_stat above).
|
|
190
|
+
We mirror rather than import because proof-generator.py has a hyphen in its
|
|
191
|
+
filename (not a plain import) and this file already mirrors its sibling's
|
|
192
|
+
hashing helpers with the same "MUST match" discipline. If the generator's
|
|
193
|
+
rules change, this copy MUST be updated in lockstep.
|
|
194
|
+
|
|
195
|
+
Redaction note: proof_redact.py only rewrites secret/path substrings inside
|
|
196
|
+
string values; it never touches the status enums, integer counts, exit_code,
|
|
197
|
+
or the shape/length of the degraded list that this function reads, and it
|
|
198
|
+
leaves bool(command) truthy. So re-deriving from the STORED (post-redaction)
|
|
199
|
+
facts yields the identical headline the generator computed pre-redaction.
|
|
200
|
+
"""
|
|
201
|
+
tests = facts.get("tests") or {}
|
|
202
|
+
build = facts.get("build") or {}
|
|
203
|
+
git = facts.get("git") or {}
|
|
204
|
+
diff_nonempty = bool((git.get("diff") or {}).get("count"))
|
|
205
|
+
|
|
206
|
+
sec = facts.get("security") or {}
|
|
207
|
+
sec_high = bool(sec.get("ran") and (sec.get("high_active") or 0) > 0)
|
|
208
|
+
any_failed = (
|
|
209
|
+
tests.get("status") == "failed"
|
|
210
|
+
or build.get("status") == "failed"
|
|
211
|
+
or any(g.get("status") == "failed"
|
|
212
|
+
for g in (facts.get("quality_gates") or []))
|
|
213
|
+
or sec_high
|
|
214
|
+
)
|
|
215
|
+
if any_failed:
|
|
216
|
+
return "NOT VERIFIED"
|
|
217
|
+
|
|
218
|
+
tests_verified = (
|
|
219
|
+
tests.get("status") == "verified"
|
|
220
|
+
and bool(tests.get("command"))
|
|
221
|
+
and tests.get("exit_code") == 0
|
|
222
|
+
)
|
|
223
|
+
if tests_verified and not degraded and diff_nonempty:
|
|
224
|
+
return "VERIFIED"
|
|
225
|
+
any_verified = (
|
|
226
|
+
tests.get("status") == "verified"
|
|
227
|
+
or build.get("status") == "verified"
|
|
228
|
+
or any(g.get("status") == "passed"
|
|
229
|
+
for g in (facts.get("quality_gates") or []))
|
|
230
|
+
)
|
|
231
|
+
if any_verified and degraded:
|
|
232
|
+
return "VERIFIED WITH GAPS"
|
|
233
|
+
return "NOT VERIFIED"
|
|
234
|
+
|
|
235
|
+
|
|
168
236
|
# ---------------------------------------------------------------------------
|
|
169
237
|
# proof field extraction (schema v1.0 + v1.1 tolerant)
|
|
170
238
|
# ---------------------------------------------------------------------------
|
|
@@ -236,6 +304,31 @@ def _recorded_degraded(proof):
|
|
|
236
304
|
return []
|
|
237
305
|
|
|
238
306
|
|
|
307
|
+
def _recorded_degraded_raw(proof):
|
|
308
|
+
"""Return honesty.degraded EXACTLY as recorded (for headline re-derivation).
|
|
309
|
+
|
|
310
|
+
_recorded_degraded coerces items to str for the report; _compute_headline
|
|
311
|
+
only cares whether the list is empty, so we pass the raw list to preserve
|
|
312
|
+
the generator's exact truthiness semantics.
|
|
313
|
+
"""
|
|
314
|
+
honesty = proof.get("honesty")
|
|
315
|
+
if isinstance(honesty, dict):
|
|
316
|
+
deg = honesty.get("degraded")
|
|
317
|
+
if isinstance(deg, list):
|
|
318
|
+
return deg
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _recorded_headline(proof):
|
|
323
|
+
"""Return honesty.headline as recorded (stripped str), or None if absent."""
|
|
324
|
+
honesty = proof.get("honesty")
|
|
325
|
+
if isinstance(honesty, dict):
|
|
326
|
+
h = honesty.get("headline")
|
|
327
|
+
if isinstance(h, str) and h.strip():
|
|
328
|
+
return h.strip()
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
|
|
239
332
|
# ---------------------------------------------------------------------------
|
|
240
333
|
# gpg
|
|
241
334
|
# ---------------------------------------------------------------------------
|
|
@@ -316,19 +409,38 @@ def verify(proof_path, repo_dir="."):
|
|
|
316
409
|
|
|
317
410
|
Returns a dict:
|
|
318
411
|
{
|
|
319
|
-
hash_ok:
|
|
320
|
-
diff_drift:
|
|
412
|
+
hash_ok: bool tamper check passed
|
|
413
|
+
diff_drift: bool | None True=drift, False=match,
|
|
321
414
|
None=could not check
|
|
322
|
-
diff_recheck:
|
|
323
|
-
gpg_ok:
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
415
|
+
diff_recheck: {recorded, current} the two diff stats compared
|
|
416
|
+
gpg_ok: True | False | "n/a" signature verdict
|
|
417
|
+
generator_trusted: bool see note below
|
|
418
|
+
headline_consistent: bool | None see note below
|
|
419
|
+
degraded: [str] honesty.degraded from the proof
|
|
420
|
+
reason: str why ok is False (when it is)
|
|
421
|
+
ok: bool overall verdict
|
|
327
422
|
}
|
|
328
423
|
|
|
329
|
-
`ok` = hash_ok AND diff_drift is False AND gpg_ok in (True, "n/a")
|
|
424
|
+
`ok` = hash_ok AND diff_drift is False AND gpg_ok in (True, "n/a")
|
|
425
|
+
AND headline_consistent is not False.
|
|
330
426
|
Note: diff_drift None (unverifiable) makes ok False, by design -- we never
|
|
331
427
|
report "verified" when the central fact could not be re-checked.
|
|
428
|
+
|
|
429
|
+
generator_trusted: True on the UNSIGNED path (gpg_ok != True), False when a
|
|
430
|
+
valid signature is present (gpg_ok is True). On the unsigned path the facts
|
|
431
|
+
are taken at face value; neutral non-forgeability is NOT guaranteed. This
|
|
432
|
+
field exists so the report can state that honestly even when ok is True (at
|
|
433
|
+
which point `reason` is cleared).
|
|
434
|
+
|
|
435
|
+
headline_consistent: defense-in-depth. We re-derive the honesty headline
|
|
436
|
+
from the RECORDED facts (same logic as proof-generator._compute_headline)
|
|
437
|
+
and compare it to the stored honesty.headline. False means an INCONSISTENT
|
|
438
|
+
edit -- e.g. the headline was flipped to VERIFIED while the facts still say
|
|
439
|
+
not_run. That catches a careless/partial forgery. It does NOT catch a
|
|
440
|
+
CONSISTENT forger who rewrites both the facts AND the headline to a matching
|
|
441
|
+
lie and recomputes the integrity hash: on the unsigned path that still
|
|
442
|
+
passes (generator_trusted stays True). None means we could not re-derive
|
|
443
|
+
(no recorded headline, or no facts to derive from) -- not a failure.
|
|
332
444
|
"""
|
|
333
445
|
proof = _load_proof(proof_path)
|
|
334
446
|
|
|
@@ -337,6 +449,8 @@ def verify(proof_path, repo_dir="."):
|
|
|
337
449
|
"diff_drift": None,
|
|
338
450
|
"diff_recheck": {"recorded": None, "current": None},
|
|
339
451
|
"gpg_ok": "n/a",
|
|
452
|
+
"generator_trusted": True,
|
|
453
|
+
"headline_consistent": None,
|
|
340
454
|
"degraded": _recorded_degraded(proof),
|
|
341
455
|
"reason": "",
|
|
342
456
|
"ok": False,
|
|
@@ -366,6 +480,12 @@ def verify(proof_path, repo_dir="."):
|
|
|
366
480
|
gpg_sig = verification.get("gpg_signature")
|
|
367
481
|
result["gpg_ok"] = _verify_gpg(canonical_bytes, gpg_sig)
|
|
368
482
|
|
|
483
|
+
# generator_trusted: only a VALID signature (gpg_ok is True) means the
|
|
484
|
+
# generator is NOT trusted (neutral non-forgeability). "n/a" or a bad sig
|
|
485
|
+
# leaves the generator trusted -- the facts are taken at face value and a
|
|
486
|
+
# consistent forger is NOT caught. Stated so ok=True still discloses it.
|
|
487
|
+
result["generator_trusted"] = (result["gpg_ok"] is not True)
|
|
488
|
+
|
|
369
489
|
# ----- 2. DRIFT CHECK --------------------------------------------------
|
|
370
490
|
recorded_stat = _recorded_diff_stat(proof)
|
|
371
491
|
result["diff_recheck"]["recorded"] = recorded_stat
|
|
@@ -439,11 +559,37 @@ def verify(proof_path, repo_dir="."):
|
|
|
439
559
|
if drift and not result["reason"]:
|
|
440
560
|
result["reason"] = "recorded diff no longer matches the repo (drift detected)"
|
|
441
561
|
|
|
562
|
+
# ----- 4. HEADLINE CONSISTENCY (defense-in-depth) ----------------------
|
|
563
|
+
# Re-derive the headline from the recorded facts and compare to the stored
|
|
564
|
+
# honesty.headline. A mismatch means the headline was edited to disagree
|
|
565
|
+
# with the facts it claims to summarize (an INCONSISTENT forgery, e.g.
|
|
566
|
+
# headline flipped to VERIFIED while facts.tests.status is still not_run).
|
|
567
|
+
# This catches careless/partial tampering only. It does NOT catch a
|
|
568
|
+
# CONSISTENT forger who rewrites both the facts and the headline to a
|
|
569
|
+
# matching lie and recomputes the integrity hash -- on the unsigned path
|
|
570
|
+
# that still passes (see generator_trusted). Neutral non-forgeability needs
|
|
571
|
+
# the signed record, not this check.
|
|
572
|
+
recorded_headline = _recorded_headline(proof)
|
|
573
|
+
facts = proof.get("facts")
|
|
574
|
+
if recorded_headline is not None and isinstance(facts, dict):
|
|
575
|
+
derived = _compute_headline(facts, _recorded_degraded_raw(proof))
|
|
576
|
+
result["headline_consistent"] = (derived == recorded_headline)
|
|
577
|
+
if not result["headline_consistent"] and not result["reason"]:
|
|
578
|
+
result["reason"] = (
|
|
579
|
+
"honesty.headline (%r) disagrees with the headline re-derived "
|
|
580
|
+
"from the recorded facts (%r); the headline was edited to "
|
|
581
|
+
"misrepresent the facts" % (recorded_headline, derived)
|
|
582
|
+
)
|
|
583
|
+
else:
|
|
584
|
+
# No recorded headline, or no facts to re-derive from: cannot check.
|
|
585
|
+
result["headline_consistent"] = None
|
|
586
|
+
|
|
442
587
|
# ----- overall verdict -------------------------------------------------
|
|
443
588
|
result["ok"] = bool(
|
|
444
589
|
result["hash_ok"]
|
|
445
590
|
and result["diff_drift"] is False
|
|
446
591
|
and result["gpg_ok"] in (True, "n/a")
|
|
592
|
+
and result["headline_consistent"] is not False
|
|
447
593
|
)
|
|
448
594
|
if result["ok"]:
|
|
449
595
|
result["reason"] = ""
|
|
@@ -12,7 +12,7 @@ Patterns implemented (see R1-proof-of-run-PLAN.md REDACTION RULES):
|
|
|
12
12
|
- Anthropic keys (sk-ant-...) -> [REDACTED:ANTHROPIC_KEY]
|
|
13
13
|
- OpenAI-style keys (sk-...) -> [REDACTED:OPENAI_KEY]
|
|
14
14
|
- Google API keys (AI...) -> [REDACTED:GOOGLE_KEY]
|
|
15
|
-
- GitHub tokens (gh[pousr]_)
|
|
15
|
+
- GitHub tokens (gh[pousr]_ and github_pat_) -> [REDACTED:GITHUB_TOKEN]
|
|
16
16
|
- AWS access key ids (AKIA..) -> [REDACTED:AWS_KEY]
|
|
17
17
|
- AWS secret access keys -> [REDACTED:AWS_SECRET]
|
|
18
18
|
- Slack tokens (xox[baprs]-) -> [REDACTED:SLACK_TOKEN]
|
|
@@ -59,6 +59,11 @@ def reset_context():
|
|
|
59
59
|
_PATTERNS = [
|
|
60
60
|
# Anthropic keys must precede the generic sk- rule.
|
|
61
61
|
(re.compile(r"sk-ant-[A-Za-z0-9_-]{20,}"), "[REDACTED:ANTHROPIC_KEY]"),
|
|
62
|
+
# GitHub fine-grained PATs (github_pat_...). Must precede the classic
|
|
63
|
+
# gh[pousr]_ rule: "github_pat_" does not match gh[pousr]_ (the char after
|
|
64
|
+
# "gh" is "i"), so the classic rule would leave fine-grained PATs untouched.
|
|
65
|
+
# The token body is a mix of [A-Za-z0-9_], so allow underscores here.
|
|
66
|
+
(re.compile(r"github_pat_[A-Za-z0-9_]{20,}"), "[REDACTED:GITHUB_TOKEN]"),
|
|
62
67
|
# GitHub tokens: ghp_, gho_, ghu_, ghs_, ghr_.
|
|
63
68
|
(re.compile(r"gh[pousr]_[A-Za-z0-9]{20,}"), "[REDACTED:GITHUB_TOKEN]"),
|
|
64
69
|
# Slack tokens: xoxb-, xoxa-, xoxp-, xoxr-, xoxs-.
|
package/autonomy/loki
CHANGED
|
@@ -31119,7 +31119,10 @@ PYEOF
|
|
|
31119
31119
|
# canonical proof (tamper check) and re-derives the diff from the
|
|
31120
31120
|
# recorded base_sha vs live HEAD (drift check). Exit 0 = clean, 1 =
|
|
31121
31121
|
# tamper/drift, 2 = unusable input. This is the "verify it yourself"
|
|
31122
|
-
# path
|
|
31122
|
+
# path: it re-checks integrity + re-derives the diff from git, so an
|
|
31123
|
+
# inconsistent edit is caught. It does NOT prove non-forgeability on
|
|
31124
|
+
# the unsigned path (a hand-forger can rewrite facts and re-hash);
|
|
31125
|
+
# neutral non-forgeability requires the signed record.
|
|
31123
31126
|
local id="${1:-}"
|
|
31124
31127
|
if [ -z "$id" ]; then
|
|
31125
31128
|
echo -e "${RED}Missing proof id.${NC} Use 'loki proof list'."
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -1180,6 +1180,16 @@ async def get_status() -> StatusResponse:
|
|
|
1180
1180
|
if state_file.exists():
|
|
1181
1181
|
try:
|
|
1182
1182
|
state = _safe_json_read(state_file, {})
|
|
1183
|
+
# dashboard-state.json is agent/user-written and, under atomic-write
|
|
1184
|
+
# races or hand edits, its top level can be a list/string/number
|
|
1185
|
+
# rather than an object. state.get(...) would then raise
|
|
1186
|
+
# AttributeError, which the surrounding (JSONDecodeError, KeyError)
|
|
1187
|
+
# handler does NOT catch -> /api/status 500s and blanks the board.
|
|
1188
|
+
# Coerce to {} so a bad shape degrades to the idle payload. Placed
|
|
1189
|
+
# BEFORE the _has_dashboard_state flag so a truthy non-dict (e.g.
|
|
1190
|
+
# [1,2,3]) cannot flip it on with corrupt data.
|
|
1191
|
+
if not isinstance(state, dict):
|
|
1192
|
+
state = {}
|
|
1183
1193
|
if state:
|
|
1184
1194
|
_has_dashboard_state = True
|
|
1185
1195
|
phase = state.get("phase", "")
|
|
@@ -1188,8 +1198,15 @@ async def get_status() -> StatusResponse:
|
|
|
1188
1198
|
mode = state.get("mode", "")
|
|
1189
1199
|
# Count only agents with alive PIDs (not raw array length)
|
|
1190
1200
|
agents_list = state.get("agents", [])
|
|
1201
|
+
if not isinstance(agents_list, list):
|
|
1202
|
+
agents_list = []
|
|
1191
1203
|
running_agents = 0
|
|
1192
1204
|
for agent in agents_list:
|
|
1205
|
+
# agents entries can be bare strings/None in malformed state;
|
|
1206
|
+
# agent.get(...) would raise AttributeError. Skip non-dicts.
|
|
1207
|
+
if not isinstance(agent, dict):
|
|
1208
|
+
running_agents += 1 # legacy/opaque entry -> count as running
|
|
1209
|
+
continue
|
|
1193
1210
|
agent_pid = agent.get("pid")
|
|
1194
1211
|
if agent_pid:
|
|
1195
1212
|
try:
|
|
@@ -1202,10 +1219,17 @@ async def get_status() -> StatusResponse:
|
|
|
1202
1219
|
running_agents += 1
|
|
1203
1220
|
|
|
1204
1221
|
tasks = state.get("tasks", {})
|
|
1205
|
-
|
|
1222
|
+
if not isinstance(tasks, dict):
|
|
1223
|
+
tasks = {}
|
|
1224
|
+
_pending = tasks.get("pending", [])
|
|
1225
|
+
pending_tasks = len(_pending) if isinstance(_pending, list) else 0
|
|
1206
1226
|
in_progress = tasks.get("inProgress", [])
|
|
1207
|
-
if in_progress:
|
|
1208
|
-
|
|
1227
|
+
if not isinstance(in_progress, list):
|
|
1228
|
+
in_progress = []
|
|
1229
|
+
if in_progress and isinstance(in_progress[0], dict):
|
|
1230
|
+
_payload = in_progress[0].get("payload", {})
|
|
1231
|
+
if isinstance(_payload, dict):
|
|
1232
|
+
current_task = _payload.get("action", "")
|
|
1209
1233
|
except (json.JSONDecodeError, KeyError):
|
|
1210
1234
|
pass
|
|
1211
1235
|
|
|
@@ -1783,6 +1807,13 @@ async def list_tasks(
|
|
|
1783
1807
|
if state_file.exists():
|
|
1784
1808
|
try:
|
|
1785
1809
|
state = json.loads(state_file.read_text())
|
|
1810
|
+
# dashboard-state.json top level can itself be a list/string/number
|
|
1811
|
+
# (agent/user-written, atomic-write race). state.get(...) would then
|
|
1812
|
+
# raise AttributeError BEFORE the task_groups guard below, and the
|
|
1813
|
+
# surrounding (JSONDecodeError, KeyError) handler does not catch it
|
|
1814
|
+
# -> /api/tasks 500s and blanks the board. Coerce state first.
|
|
1815
|
+
if not isinstance(state, dict):
|
|
1816
|
+
state = {}
|
|
1786
1817
|
task_groups = state.get("tasks", {})
|
|
1787
1818
|
# v7.104.4: dashboard-state.json is user/agent-written and can be
|
|
1788
1819
|
# malformed or partially written. If "tasks" is not a dict, or a
|
|
@@ -6621,6 +6652,11 @@ def _compute_cost_snapshot() -> dict:
|
|
|
6621
6652
|
if budget_file.exists():
|
|
6622
6653
|
try:
|
|
6623
6654
|
budget_data = json.loads(budget_file.read_text())
|
|
6655
|
+
# budget.json can be a bare number/list/string; guard before .get()
|
|
6656
|
+
# so the cost-summary endpoint degrades instead of raising an
|
|
6657
|
+
# AttributeError uncaught by (JSONDecodeError, KeyError).
|
|
6658
|
+
if not isinstance(budget_data, dict):
|
|
6659
|
+
budget_data = {}
|
|
6624
6660
|
budget_limit = budget_data.get("limit")
|
|
6625
6661
|
if budget_limit is not None:
|
|
6626
6662
|
budget_used = estimated_cost
|
|
@@ -6664,6 +6700,12 @@ async def get_budget():
|
|
|
6664
6700
|
if budget_file.exists():
|
|
6665
6701
|
try:
|
|
6666
6702
|
budget_data = json.loads(budget_file.read_text())
|
|
6703
|
+
# budget.json can be a bare number/list/string (agent/user-written
|
|
6704
|
+
# or atomic-write race), so budget_data.get(...) would raise
|
|
6705
|
+
# AttributeError, uncaught by (JSONDecodeError, KeyError) -> 500.
|
|
6706
|
+
# Coerce to {} so the endpoint degrades to no-limit defaults.
|
|
6707
|
+
if not isinstance(budget_data, dict):
|
|
6708
|
+
budget_data = {}
|
|
6667
6709
|
budget_limit = budget_data.get("limit") or budget_data.get("budget_limit")
|
|
6668
6710
|
budget_used = budget_data.get("budget_used", 0.0)
|
|
6669
6711
|
exceeded = budget_data.get("exceeded", False)
|
|
@@ -6687,6 +6729,9 @@ async def get_budget():
|
|
|
6687
6729
|
if exceeded_at is None:
|
|
6688
6730
|
try:
|
|
6689
6731
|
sig_data = json.loads(signal_file.read_text())
|
|
6732
|
+
# Signal file may parse to a non-dict; guard before .get().
|
|
6733
|
+
if not isinstance(sig_data, dict):
|
|
6734
|
+
sig_data = {}
|
|
6690
6735
|
exceeded_at = sig_data.get("timestamp")
|
|
6691
6736
|
except (json.JSONDecodeError, KeyError):
|
|
6692
6737
|
pass
|
|
@@ -10935,9 +10980,17 @@ async def proofs_summary():
|
|
|
10935
10980
|
"""Honest aggregate over the active project's Evidence Receipts.
|
|
10936
10981
|
|
|
10937
10982
|
Counts are computed ONLY from real proof.json files; nothing is invented.
|
|
10938
|
-
|
|
10939
|
-
|
|
10940
|
-
|
|
10983
|
+
Each proof is bucketed on its recorded honesty.headline, computed by the
|
|
10984
|
+
deterministic proof-generator.py::_compute_headline (never an LLM opinion).
|
|
10985
|
+
Honest scope of that guarantee: the integrity hash proves the JSON bytes
|
|
10986
|
+
were not edited since they were hashed, and proof-verify.py re-derives the
|
|
10987
|
+
git diff so a skeptic can confirm the recorded diff still matches the repo.
|
|
10988
|
+
But on the UNSIGNED path the generator is TRUSTED -- a forger who rewrites
|
|
10989
|
+
both the facts and the headline to a mutually consistent lie and recomputes
|
|
10990
|
+
the hash still buckets as verified here. Neutral, adversarial non-forgeability
|
|
10991
|
+
(the generator is not trusted) requires the SIGNED record (a gpg signature
|
|
10992
|
+
proof-verify.py checks). This endpoint reports what the generator recorded;
|
|
10993
|
+
it does not itself re-verify. Buckets:
|
|
10941
10994
|
|
|
10942
10995
|
verified -> honesty.headline == "VERIFIED"
|
|
10943
10996
|
with_gaps -> honesty.headline == "VERIFIED WITH GAPS"
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.
|
|
5
|
+
**Version:** v7.111.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var X8=Object.defineProperty;var K8=($)=>$;function q8($,Q){this[$]=K8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)X8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:q8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var v1={};b(v1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as r,dirname as i$}from"path";import{fileURLToPath as J8}from"url";import{existsSync as x$}from"fs";import{homedir as V8}from"os";function W8(){let $=y1;for(let Q=0;Q<6;Q++){if(x$(r($,"VERSION"))&&x$(r($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return r(y1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(x$(r(Q,"VERSION"))&&x$(r(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return r($,"..","..","..")}function P(){return process.env.LOKI_DIR??r(process.cwd(),".loki")}function T$(){return r(V8(),".loki")}var y1,h;var C=L(()=>{y1=i$(J8(import.meta.url));h=W8()});import{readFileSync as U8}from"fs";import{resolve as H8,dirname as G8}from"path";import{fileURLToPath as B8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.
|
|
2
|
+
var X8=Object.defineProperty;var K8=($)=>$;function q8($,Q){this[$]=K8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)X8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:q8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var v1={};b(v1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as r,dirname as i$}from"path";import{fileURLToPath as J8}from"url";import{existsSync as x$}from"fs";import{homedir as V8}from"os";function W8(){let $=y1;for(let Q=0;Q<6;Q++){if(x$(r($,"VERSION"))&&x$(r($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return r(y1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(x$(r(Q,"VERSION"))&&x$(r(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return r($,"..","..","..")}function P(){return process.env.LOKI_DIR??r(process.cwd(),".loki")}function T$(){return r(V8(),".loki")}var y1,h;var C=L(()=>{y1=i$(J8(import.meta.url));h=W8()});import{readFileSync as U8}from"fs";import{resolve as H8,dirname as G8}from"path";import{fileURLToPath as B8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.111.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=G8(B8(import.meta.url)),Z=e$(Q);Q$=U8(H8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var $1=L(()=>{C()});var c1={};b(c1,{runOrThrow:()=>x8,run:()=>F,readStreamCapped:()=>u1,commandVersion:()=>S8,commandExists:()=>f,ShellError:()=>Q1,MAX_STDOUT_BYTES:()=>f1});async function u1($,Q=f1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([u1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function x8($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new Q1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=N8($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function N8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function S8($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var f1=16777216,Q1;var d=L(()=>{Q1=class Q1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function t($){return D8?"":$}var D8,T,S,_,nZ,I,k,y,J;var c=L(()=>{D8=(process.env.NO_COLOR??"").length>0;T=t("\x1B[0;31m"),S=t("\x1B[0;32m"),_=t("\x1B[1;33m"),nZ=t("\x1B[0;34m"),I=t("\x1B[0;36m"),k=t("\x1B[1m"),y=t("\x1B[2m"),J=t("\x1B[0m")});import{existsSync as c8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(c8($))return A$=$,$;let Q=await f("python3.12");if(Q)return A$=Q,Q;let Z=await f("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var A$;var V$=L(()=>{d()});var J0={};b(J0,{runStatus:()=>U3});import{existsSync as v,readFileSync as U$,readdirSync as e1,statSync as $0}from"fs";import{resolve as D,basename as Q3}from"path";import{homedir as Z3}from"os";function Q0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function Z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*D$/Q);if(X>D$)X=D$;let q=D$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Q0($),U=Q0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${U})`}async function X3(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -805,4 +805,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
805
805
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
806
806
|
`),process.stderr.write(z8),2}}i1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var EZ=await RZ(Bun.argv.slice(2));process.exit(EZ);
|
|
807
807
|
|
|
808
|
-
//# debugId=
|
|
808
|
+
//# debugId=75C1D788DA07ECAB64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/mcp/lsp_proxy.py
CHANGED
|
@@ -1478,6 +1478,95 @@ def write_diagnostics_artifact(root: Optional[str] = None,
|
|
|
1478
1478
|
'count_warnings': count_warnings}
|
|
1479
1479
|
|
|
1480
1480
|
|
|
1481
|
+
def _run_http_transport(port: int) -> None:
|
|
1482
|
+
"""Run the LSP-proxy MCP server over Streamable HTTP, bound to loopback.
|
|
1483
|
+
|
|
1484
|
+
Mirrors mcp/server.py:_run_http_transport for consistency. FastMCP has no
|
|
1485
|
+
'http' transport literal (it uses 'streamable-http') and run() takes no
|
|
1486
|
+
port= kwarg, so the old mcp.run(transport='http', port=...) call never
|
|
1487
|
+
worked (TypeError). We build the Streamable-HTTP ASGI app ourselves, bind
|
|
1488
|
+
127.0.0.1 explicitly, and run it via uvicorn -- the same supported path.
|
|
1489
|
+
|
|
1490
|
+
Two hardening properties over a bare FastMCP HTTP launch:
|
|
1491
|
+
|
|
1492
|
+
1. Explicit loopback bind. We set host='127.0.0.1' ourselves and run the
|
|
1493
|
+
ASGI app via uvicorn rather than relying on any implicit default, so a
|
|
1494
|
+
reader of `lsof`/`ss` sees 127.0.0.1 unambiguously and a future SDK
|
|
1495
|
+
default change cannot silently widen the bind to 0.0.0.0.
|
|
1496
|
+
|
|
1497
|
+
2. Optional bearer-token auth. When LOKI_MCP_AUTH_TOKEN is set in the
|
|
1498
|
+
environment, a Starlette middleware requires every HTTP request to
|
|
1499
|
+
carry `Authorization: Bearer <token>` (rejecting mismatches with 401).
|
|
1500
|
+
When the env var is unset or empty, NO middleware is installed at all,
|
|
1501
|
+
so the request path is exactly the unauthenticated FastMCP behavior.
|
|
1502
|
+
|
|
1503
|
+
hasattr-guarded against the whole supported mcp 1.x range: if the installed
|
|
1504
|
+
SDK lacks streamable_http_app, we fail with a clear message rather than an
|
|
1505
|
+
AttributeError.
|
|
1506
|
+
"""
|
|
1507
|
+
if not hasattr(mcp, "streamable_http_app"):
|
|
1508
|
+
logger.error(
|
|
1509
|
+
"Installed MCP SDK does not support Streamable HTTP transport "
|
|
1510
|
+
"(no FastMCP.streamable_http_app). Upgrade the 'mcp' package "
|
|
1511
|
+
"(pip install -U mcp) or use the default stdio transport."
|
|
1512
|
+
)
|
|
1513
|
+
sys.exit(1)
|
|
1514
|
+
|
|
1515
|
+
host = "127.0.0.1"
|
|
1516
|
+
|
|
1517
|
+
# Pin host/port on the SDK settings too, so any code that reads them (and
|
|
1518
|
+
# the SDK's own transport-security allowed_hosts, which defaults to
|
|
1519
|
+
# 127.0.0.1/localhost) stays consistent with what uvicorn actually binds.
|
|
1520
|
+
try:
|
|
1521
|
+
if hasattr(mcp, "settings"):
|
|
1522
|
+
if hasattr(mcp.settings, "host"):
|
|
1523
|
+
mcp.settings.host = host
|
|
1524
|
+
if hasattr(mcp.settings, "port"):
|
|
1525
|
+
mcp.settings.port = port
|
|
1526
|
+
except Exception as _set_err: # pragma: no cover - defensive
|
|
1527
|
+
logger.warning("Could not pin MCP host/port settings: %s", _set_err)
|
|
1528
|
+
|
|
1529
|
+
app = mcp.streamable_http_app()
|
|
1530
|
+
|
|
1531
|
+
token = os.environ.get("LOKI_MCP_AUTH_TOKEN", "")
|
|
1532
|
+
if token:
|
|
1533
|
+
# Only import the middleware pieces when auth is actually requested, so
|
|
1534
|
+
# the unauthenticated path has zero new dependencies or behavior.
|
|
1535
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
1536
|
+
from starlette.responses import JSONResponse
|
|
1537
|
+
|
|
1538
|
+
expected_header = "Bearer " + token
|
|
1539
|
+
|
|
1540
|
+
async def _require_bearer(request, call_next):
|
|
1541
|
+
# BaseHTTPMiddleware auto-forwards non-http scopes (lifespan etc.),
|
|
1542
|
+
# so the StreamableHTTPSessionManager lifespan still starts.
|
|
1543
|
+
provided = request.headers.get("authorization", "")
|
|
1544
|
+
# Constant-time compare to avoid leaking the token via timing.
|
|
1545
|
+
import hmac
|
|
1546
|
+
if not hmac.compare_digest(provided, expected_header):
|
|
1547
|
+
return JSONResponse(
|
|
1548
|
+
{"error": "unauthorized"},
|
|
1549
|
+
status_code=401,
|
|
1550
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
1551
|
+
)
|
|
1552
|
+
return await call_next(request)
|
|
1553
|
+
|
|
1554
|
+
app.add_middleware(BaseHTTPMiddleware, dispatch=_require_bearer)
|
|
1555
|
+
logger.info(
|
|
1556
|
+
"MCP HTTP auth enabled: bearer token required "
|
|
1557
|
+
"(LOKI_MCP_AUTH_TOKEN is set)."
|
|
1558
|
+
)
|
|
1559
|
+
else:
|
|
1560
|
+
logger.info(
|
|
1561
|
+
"MCP HTTP auth disabled: no LOKI_MCP_AUTH_TOKEN set "
|
|
1562
|
+
"(loopback-only, unauthenticated)."
|
|
1563
|
+
)
|
|
1564
|
+
|
|
1565
|
+
import uvicorn
|
|
1566
|
+
logger.info("MCP Streamable HTTP listening on http://%s:%d/mcp", host, port)
|
|
1567
|
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
1568
|
+
|
|
1569
|
+
|
|
1481
1570
|
def main() -> None:
|
|
1482
1571
|
import argparse
|
|
1483
1572
|
parser = argparse.ArgumentParser(
|
|
@@ -1536,7 +1625,11 @@ def main() -> None:
|
|
|
1536
1625
|
detected = _detect_lsps()
|
|
1537
1626
|
logger.info("Detected LSPs: %s", sorted(detected.keys()) or 'none')
|
|
1538
1627
|
if args.transport == 'http':
|
|
1539
|
-
|
|
1628
|
+
# Explicit loopback bind + optional bearer-token auth. FastMCP has no
|
|
1629
|
+
# 'http' transport literal (it uses 'streamable-http') and run() takes
|
|
1630
|
+
# no port= kwarg, so the old mcp.run(transport='http', port=...) call
|
|
1631
|
+
# never worked (TypeError); _run_http_transport is the supported path.
|
|
1632
|
+
_run_http_transport(args.port)
|
|
1540
1633
|
else:
|
|
1541
1634
|
mcp.run(transport='stdio')
|
|
1542
1635
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.111.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.
|
|
5
|
+
"version": "7.111.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|
package/providers/aider.sh
CHANGED
|
@@ -58,9 +58,9 @@ _aider_default_from_catalog() {
|
|
|
58
58
|
if [ -f "${script_dir}/models.sh" ]; then
|
|
59
59
|
# shellcheck source=./models.sh
|
|
60
60
|
source "${script_dir}/models.sh"
|
|
61
|
-
loki_latest_model aider development 2>/dev/null || echo "claude-opus-4-
|
|
61
|
+
loki_latest_model aider development 2>/dev/null || echo "claude-opus-4-8"
|
|
62
62
|
else
|
|
63
|
-
echo "claude-opus-4-
|
|
63
|
+
echo "claude-opus-4-8"
|
|
64
64
|
fi
|
|
65
65
|
}
|
|
66
66
|
AIDER_DEFAULT_MODEL="${LOKI_AIDER_MODEL:-${LOKI_MODEL_DEVELOPMENT:-$(_aider_default_from_catalog)}}"
|
package/providers/claude.sh
CHANGED
|
@@ -48,7 +48,7 @@ PROVIDER_MAX_PARALLEL=10
|
|
|
48
48
|
# The Claude Code CLI resolves aliases (opus/sonnet/haiku) to the latest available
|
|
49
49
|
# model at invocation time, so we pass aliases rather than dated IDs. The canonical
|
|
50
50
|
# mapping lives in providers/model_catalog.json (single source of truth):
|
|
51
|
-
# opus -> latest Opus (e.g. claude-opus-4-
|
|
51
|
+
# opus -> latest Opus (e.g. claude-opus-4-8 -- 1M context, adaptive thinking)
|
|
52
52
|
# sonnet -> latest Sonnet (e.g. claude-sonnet-4-6)
|
|
53
53
|
# haiku -> latest Haiku (e.g. claude-haiku-4-5)
|
|
54
54
|
# Override per tier with LOKI_CLAUDE_MODEL_PLANNING, _DEVELOPMENT, _FAST.
|
package/providers/cline.sh
CHANGED
|
@@ -59,9 +59,9 @@ _cline_default_from_catalog() {
|
|
|
59
59
|
if [ -f "${script_dir}/models.sh" ]; then
|
|
60
60
|
# shellcheck source=./models.sh
|
|
61
61
|
source "${script_dir}/models.sh"
|
|
62
|
-
loki_latest_model cline development 2>/dev/null || echo "claude-opus-4-
|
|
62
|
+
loki_latest_model cline development 2>/dev/null || echo "claude-opus-4-8"
|
|
63
63
|
else
|
|
64
|
-
echo "claude-opus-4-
|
|
64
|
+
echo "claude-opus-4-8"
|
|
65
65
|
fi
|
|
66
66
|
}
|
|
67
67
|
CLINE_DEFAULT_MODEL="${LOKI_CLINE_MODEL:-${LOKI_MODEL_DEVELOPMENT:-$(_cline_default_from_catalog)}}"
|
package/providers/codex.sh
CHANGED
|
@@ -234,21 +234,29 @@ provider_invoke_with_tier() {
|
|
|
234
234
|
*) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
|
|
235
235
|
esac
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
# --search is a TOP-LEVEL `codex` flag, not a `codex exec` flag. On codex
|
|
238
|
+
# 0.141.0 `codex exec ... --search` aborts with "unexpected argument
|
|
239
|
+
# '--search' found", silently breaking LOKI_CODEX_WEB_SEARCH. It must be
|
|
240
|
+
# placed before the `exec` subcommand. --output-last-message (-o) IS an
|
|
241
|
+
# `codex exec` flag and stays after `exec`. Keep the two in separate arrays.
|
|
242
|
+
local pre_exec_flags=()
|
|
238
243
|
if [ "${LOKI_CODEX_WEB_SEARCH:-false}" = "true" ]; then
|
|
239
|
-
|
|
244
|
+
pre_exec_flags+=(--search)
|
|
240
245
|
fi
|
|
246
|
+
local extra_flags=()
|
|
241
247
|
if [ "${LOKI_CODEX_OUTPUT_LAST:-true}" != "false" ] && [ -n "${LOKI_LOG_FILE:-}" ]; then
|
|
242
248
|
extra_flags+=(--output-last-message "${LOKI_LOG_FILE}.last-message")
|
|
243
249
|
fi
|
|
244
250
|
|
|
245
251
|
LOKI_CODEX_REASONING_EFFORT="$effort" \
|
|
246
252
|
CODEX_MODEL_REASONING_EFFORT="$effort" \
|
|
247
|
-
# Guard the
|
|
248
|
-
#
|
|
253
|
+
# Guard the array expansions: with no web-search / output-last knobs an
|
|
254
|
+
# array is empty, and a bare "${arr[@]}" under `set -u` aborts with
|
|
249
255
|
# "unbound variable" on bash 3.2 (stock macOS /bin/bash). ${arr[@]+...}
|
|
250
256
|
# expands to nothing when empty and preserves spaced elements otherwise.
|
|
251
|
-
codex
|
|
257
|
+
codex \
|
|
258
|
+
"${pre_exec_flags[@]+"${pre_exec_flags[@]}"}" \
|
|
259
|
+
exec \
|
|
252
260
|
--sandbox workspace-write \
|
|
253
261
|
--skip-git-repo-check \
|
|
254
262
|
--model "$model" \
|
package/providers/models.sh
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Usage:
|
|
10
10
|
# source providers/models.sh
|
|
11
|
-
# model=$(loki_latest_model claude planning) # -> claude-opus-4-
|
|
11
|
+
# model=$(loki_latest_model claude planning) # -> claude-opus-4-8
|
|
12
12
|
#
|
|
13
13
|
# Env override order: LOKI_<PROVIDER>_MODEL_<TIER> > LOKI_<PROVIDER>_MODEL > catalog latest.
|
|
14
14
|
|