loki-mode 11.66.2 → 11.67.1

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.
Files changed (76) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/route.sh +3 -2
  5. package/dashboard/__init__.py +1 -1
  6. package/docs/CLI-REFERENCE.md +1 -1
  7. package/docs/COMPONENTS.md +1 -1
  8. package/docs/INSTALLATION.md +1 -1
  9. package/docs/WANG-PRINCIPLES-PLAN.md +1 -1
  10. package/docs/v10/FAILURE-CLASSES.md +32 -0
  11. package/loki-ts/dist/loki.js +1283 -1275
  12. package/mcp/__init__.py +1 -1
  13. package/mcp/server.py +31 -1
  14. package/mcp/tests/test_read_only.py +1 -1
  15. package/mcp/tests/test_v10_tools.py +212 -0
  16. package/mcp/v10_tools.py +83 -0
  17. package/package.json +2 -2
  18. package/packages/control-plane/dist/server.js +106 -12
  19. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  20. package/web-app/dist/assets/{AdminPage-DUUSP-zy.js → AdminPage-BtVpg8_T.js} +1 -1
  21. package/web-app/dist/assets/{Avatar-B8BH9fQZ.js → Avatar-C3I7YiJ1.js} +1 -1
  22. package/web-app/dist/assets/{Badge-BXVq433G.js → Badge-DsjUF4b1.js} +1 -1
  23. package/web-app/dist/assets/{Button-BFK_qowE.js → Button-Cv_fLuDO.js} +1 -1
  24. package/web-app/dist/assets/{CockpitPage-B5uaJ0Z_.js → CockpitPage-CPccpsAb.js} +1 -1
  25. package/web-app/dist/assets/{ComparePage-BOJidJKo.js → ComparePage-Bg0PwPg5.js} +1 -1
  26. package/web-app/dist/assets/{ErrorBoundary-BSDwUoJy.js → ErrorBoundary-CCDg-ONt.js} +1 -1
  27. package/web-app/dist/assets/{EvidenceReceiptPanel-DFIrryk0.js → EvidenceReceiptPanel-8_bvJsKL.js} +1 -1
  28. package/web-app/dist/assets/{GitHubIssuesPanel-Bpy9Q7KU.js → GitHubIssuesPanel-Dx7gh4rO.js} +1 -1
  29. package/web-app/dist/assets/{GitHubPRsPanel-C40Uwyjl.js → GitHubPRsPanel-CnLE8VPU.js} +1 -1
  30. package/web-app/dist/assets/{HomePage-CrKeQ9kt.js → HomePage-zENRZ2D1.js} +2 -2
  31. package/web-app/dist/assets/{LoginPage-DZLHMRAF.js → LoginPage-BiOqKbkH.js} +1 -1
  32. package/web-app/dist/assets/{MagicPage-wSsYtbHa.js → MagicPage-jLmkP87D.js} +1 -1
  33. package/web-app/dist/assets/{MetricsPage-DKzav4Xg.js → MetricsPage-CVEoZoC6.js} +1 -1
  34. package/web-app/dist/assets/{NotFoundPage-tFWP8KUK.js → NotFoundPage-mmQRmD8R.js} +1 -1
  35. package/web-app/dist/assets/{ProjectPage-52h79KFr.js → ProjectPage-CJmupIud.js} +3 -3
  36. package/web-app/dist/assets/{ProjectsPage-DRSpM7E2.js → ProjectsPage-BARdLMPp.js} +1 -1
  37. package/web-app/dist/assets/{SettingsPage-B5_eQXf1.js → SettingsPage-Cmrmt2KR.js} +1 -1
  38. package/web-app/dist/assets/{ShowcasePage-mlk1VZ5N.js → ShowcasePage-uwsZdS5N.js} +1 -1
  39. package/web-app/dist/assets/{SystemSettingsPage-L7NP65y7.js → SystemSettingsPage-7tt-pqsE.js} +1 -1
  40. package/web-app/dist/assets/{TeamsPage-C5gbHIhR.js → TeamsPage-CQDznmbk.js} +1 -1
  41. package/web-app/dist/assets/{TemplatesPage-CpRsgu5-.js → TemplatesPage-DvUcZiJJ.js} +1 -1
  42. package/web-app/dist/assets/{TerminalOutput-C_tCR_Km.js → TerminalOutput-CsnBC0fm.js} +1 -1
  43. package/web-app/dist/assets/{activity-DDAkha-U.js → activity-B0fkdbAZ.js} +1 -1
  44. package/web-app/dist/assets/{bell-d3C_NwgU.js → bell-Cmx08Gc7.js} +1 -1
  45. package/web-app/dist/assets/{bot-xx2FrcyK.js → bot-mHlwlDBE.js} +1 -1
  46. package/web-app/dist/assets/{check-B1wjphtz.js → check-CHbtOnFG.js} +1 -1
  47. package/web-app/dist/assets/{chevron-left-CxIjKCcc.js → chevron-left-CmaIVO-9.js} +1 -1
  48. package/web-app/dist/assets/{circle-alert-BkUkszys.js → circle-alert-CleAQ1iS.js} +1 -1
  49. package/web-app/dist/assets/{clock-Da0Cxc6b.js → clock-BZNwavS8.js} +1 -1
  50. package/web-app/dist/assets/{cloud-CemmtpAa.js → cloud-DmPDssWE.js} +1 -1
  51. package/web-app/dist/assets/{code-xml-D1PFPtf5.js → code-xml-C1wP07qL.js} +1 -1
  52. package/web-app/dist/assets/{database-4tOYZHTT.js → database-Cudj812p.js} +1 -1
  53. package/web-app/dist/assets/{dollar-sign-CGwkXp1P.js → dollar-sign-ClfPDwwb.js} +1 -1
  54. package/web-app/dist/assets/{file-code-corner-Cv3xwFb6.js → file-code-corner-BjLiR_DM.js} +1 -1
  55. package/web-app/dist/assets/{file-plus-DR0S1Q6M.js → file-plus-B3S15wdF.js} +1 -1
  56. package/web-app/dist/assets/{globe-CnM5-QEh.js → globe-FLJd7IqB.js} +1 -1
  57. package/web-app/dist/assets/{hammer-BybFanZ-.js → hammer-uXcz6raP.js} +1 -1
  58. package/web-app/dist/assets/{index-CsEKqPjD.js → index-8LjkUT_r.js} +3 -3
  59. package/web-app/dist/assets/{layers-AsoI5QVF.js → layers-BPi2kYUA.js} +1 -1
  60. package/web-app/dist/assets/{loader-circle-BP-Dgm4_.js → loader-circle-BI5Jgauk.js} +1 -1
  61. package/web-app/dist/assets/{lock-9heWJfZK.js → lock-XYI8VTfk.js} +1 -1
  62. package/web-app/dist/assets/{package-B3oxVBPu.js → package-D5dabXGu.js} +1 -1
  63. package/web-app/dist/assets/{plus-Dk3jSp9T.js → plus-C3uOyi3T.js} +1 -1
  64. package/web-app/dist/assets/{refresh-cw-D_EqfAM1.js → refresh-cw-emf7OJ8L.js} +1 -1
  65. package/web-app/dist/assets/{rotate-ccw-BYWk_Sh7.js → rotate-ccw-BHrmxW3U.js} +1 -1
  66. package/web-app/dist/assets/{scroll-text-CLU6QkSK.js → scroll-text-0AFHZ8SW.js} +1 -1
  67. package/web-app/dist/assets/{server-BQVeYEi-.js → server-DNeU7_2A.js} +1 -1
  68. package/web-app/dist/assets/{shield-alert-CGdnbbuO.js → shield-alert-DnACnSDh.js} +1 -1
  69. package/web-app/dist/assets/{thumbs-up-CqAiD7w7.js → thumbs-up-BjHFuW7u.js} +1 -1
  70. package/web-app/dist/assets/{trash-2-DOZ4MX9o.js → trash-2-BdNn79gn.js} +1 -1
  71. package/web-app/dist/assets/{trending-down-vm1A3ywr.js → trending-down-LQcvJtlW.js} +1 -1
  72. package/web-app/dist/assets/{trending-up-CdGlmr5H.js → trending-up-DnAsnTeg.js} +1 -1
  73. package/web-app/dist/assets/{upload--DV5H13K.js → upload-CywYeREK.js} +1 -1
  74. package/web-app/dist/assets/{usePolling-CTvwKWlp.js → usePolling-B7sPkY8h.js} +1 -1
  75. package/web-app/dist/assets/{user-CQ-pSxyg.js → user-oYnoDRHM.js} +1 -1
  76. package/web-app/dist/index.html +1 -1
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '11.66.2'
78
+ __version__ = '11.67.1'
package/mcp/server.py CHANGED
@@ -653,7 +653,7 @@ _TRUSTED_TOOLS = frozenset({
653
653
  "loki_memory_store_pattern", "loki_task_queue_add",
654
654
  "loki_memory_capture_session_summary",
655
655
  "loki_consolidate_memory", "loki_complete_task", "loki_v10_run",
656
- "loki_v10_verify", "loki_start_project", "loki_code_search_stats",
656
+ "loki_v10_verify", "loki_seal_diff", "loki_start_project", "loki_code_search_stats",
657
657
  "loki_memory_redact",
658
658
  "loki_magic_stats", "loki_magic_tokens_extract",
659
659
  "cp_stats", "cp_cost", "cp_repos_list",
@@ -1900,6 +1900,36 @@ async def loki_v10_verify(receipt_path: str = "", repo_path: str = "") -> str:
1900
1900
  raise
1901
1901
 
1902
1902
 
1903
+ @mcp.tool()
1904
+ async def loki_seal_diff(repo_path: str, intent: str) -> str:
1905
+ """
1906
+ Seal changes you already made in a repo into a signed Loki receipt.
1907
+ Use it after editing a repo yourself, to get the same Wall-verified receipt a
1908
+ Loki run gives. The Wall is written from the intent alone, without seeing the
1909
+ diff. Do not use it to build something (use loki_v10_run) or to re-check an
1910
+ existing receipt (use loki_v10_verify).
1911
+
1912
+ Args:
1913
+ repo_path: Git repository with uncommitted changes to seal
1914
+ intent: What the changes are meant to do, in plain words
1915
+
1916
+ Returns:
1917
+ JSON with run_id, verdict, verdict_is_verified (the receipt verdict string, not a signature check), receipt_path, sealed_ref and not_proven
1918
+ """
1919
+ _emit_tool_event_async('loki_seal_diff', 'start', parameters={'repo_path': repo_path, 'intent': intent})
1920
+ try:
1921
+ from mcp import v10_tools
1922
+ result = await asyncio.to_thread(
1923
+ v10_tools.seal_diff, repo_path, intent,
1924
+ lambda p: validate_path(p, allowed_dirs=['.']))
1925
+ _emit_tool_event_async('loki_seal_diff', 'complete', result_status='error' if isinstance(result, dict) and result.get('error') else 'success')
1926
+ return json.dumps(result)
1927
+ except Exception as e:
1928
+ logger.error(f"loki_seal_diff failed: {e}")
1929
+ _emit_tool_event_async('loki_seal_diff', 'complete', result_status='error', error=str(e))
1930
+ raise
1931
+
1932
+
1903
1933
  @mcp.tool()
1904
1934
  async def loki_start_project(prd_content: str = "", prd_path: str = "") -> str:
1905
1935
  """
@@ -27,7 +27,7 @@ EXPECTED_ALLOWLIST = {
27
27
  EXCLUDED_TOOLS = {
28
28
  "loki_memory_store_pattern", "loki_task_queue_add", "loki_task_queue_update",
29
29
  "loki_memory_capture_session_summary", "loki_consolidate_memory",
30
- "loki_complete_task", "loki_v10_run", "loki_v10_verify",
30
+ "loki_complete_task", "loki_v10_run", "loki_v10_verify", "loki_seal_diff",
31
31
  "loki_start_project", "loki_checkpoint_restore", "loki_verify_fast",
32
32
  "loki_counter_evidence_template", "loki_task_queue_list",
33
33
  "loki_code_search_stats", "loki_memory_redact", "loki_graph_query",
@@ -2,6 +2,7 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ import shutil
5
6
  import subprocess
6
7
  import sys
7
8
  import tempfile
@@ -244,3 +245,214 @@ class VerifyTests(unittest.TestCase):
244
245
 
245
246
  if __name__ == "__main__":
246
247
  unittest.main()
248
+
249
+
250
+ _FAKE_LOKI = """#!/bin/sh
251
+ # stand-in engine: records what it was handed, writes a receipt with the verdict in $FAKE_VERDICT
252
+ [ "$2" = "--seal-existing" ] || exit 9
253
+ [ -z "$(git status --porcelain)" ] || exit 8
254
+ cp "$3" "$FAKE_OUT/patch"
255
+ printf '%s' "$1" > "$FAKE_OUT/intent"
256
+ RID="${FAKE_RUN_ID:-e10-fake}"
257
+ mkdir -p ".loki/runs/$RID"
258
+ printf '{"run_id":"%s"}' "$RID" > .loki/engine.json
259
+ printf '{"verdict":"%s","not_proven":["x"],"head_sha":"%s"}' "${FAKE_VERDICT:-VERIFIED}" "$(git rev-parse HEAD)" > ".loki/runs/$RID/receipt.json"
260
+ """
261
+
262
+
263
+ def _git(cwd, *args):
264
+ return subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, check=True).stdout
265
+
266
+
267
+ class SealDiffTests(unittest.TestCase):
268
+ def setUp(self):
269
+ self.tmp = tempfile.mkdtemp(prefix="loki-test-seal-")
270
+ self.addCleanup(__import__("shutil").rmtree, self.tmp, True)
271
+ self.repo = os.path.join(self.tmp, "repo")
272
+ os.makedirs(self.repo)
273
+ _git(self.repo, "init", "-q", "-b", "main")
274
+ _git(self.repo, "config", "user.name", "t")
275
+ _git(self.repo, "config", "user.email", "t@example.invalid")
276
+ with open(os.path.join(self.repo, "a.txt"), "w") as f:
277
+ f.write("base\n")
278
+ _git(self.repo, "add", "a.txt")
279
+ _git(self.repo, "commit", "-q", "-m", "base")
280
+ self.out = os.path.join(self.tmp, "out")
281
+ os.makedirs(self.out)
282
+ self.fake = os.path.join(self.tmp, "loki")
283
+ with open(self.fake, "w") as f:
284
+ f.write(_FAKE_LOKI)
285
+ os.chmod(self.fake, 0o755)
286
+ env = mock.patch.dict(os.environ, {"FAKE_OUT": self.out})
287
+ env.start()
288
+ self.addCleanup(env.stop)
289
+ bin_patch = mock.patch.object(v10_tools, "LOKI_BIN", self.fake)
290
+ bin_patch.start()
291
+ self.addCleanup(bin_patch.stop)
292
+
293
+ def _edit(self):
294
+ with open(os.path.join(self.repo, "a.txt"), "a") as f:
295
+ f.write("changed line\n")
296
+ with open(os.path.join(self.repo, "new.txt"), "w") as f:
297
+ f.write("brand new\n")
298
+
299
+ def test_seals_working_tree_diff_and_leaves_user_repo_untouched(self):
300
+ self._edit()
301
+ before = (_git(self.repo, "status", "--porcelain"), _git(self.repo, "rev-parse", "HEAD"),
302
+ _git(self.repo, "rev-parse", "--abbrev-ref", "HEAD"), _git(self.repo, "diff", "--cached"))
303
+ r = v10_tools.seal_diff(self.repo, "add a line", _ok)
304
+ self.assertNotIn("error", r)
305
+ self.assertEqual((r["verdict"], r["verdict_is_verified"], r["run_id"]), ("VERIFIED", True, "e10-fake"))
306
+ self.assertTrue(os.path.isfile(r["receipt_path"]))
307
+ patch = open(os.path.join(self.out, "patch")).read()
308
+ self.assertIn("+changed line", patch)
309
+ self.assertIn("+brand new", patch)
310
+ self.assertEqual(open(os.path.join(self.out, "intent")).read(), "add a line")
311
+ after = (_git(self.repo, "status", "--porcelain"), _git(self.repo, "rev-parse", "HEAD"),
312
+ _git(self.repo, "rev-parse", "--abbrev-ref", "HEAD"), _git(self.repo, "diff", "--cached"))
313
+ self.assertEqual(before[1:], after[1:])
314
+ self.assertEqual(before[3], "")
315
+ self.assertEqual(after[0].replace("?? .loki/\n", ""), before[0])
316
+
317
+ def test_run_id_from_the_clone_is_validated_before_any_path_join(self):
318
+ self._edit()
319
+ escape = os.path.join(self.tmp, "escaped")
320
+ for bad in (escape, "../../escaped", "a/../b", ".hidden/.."):
321
+ with mock.patch.dict(os.environ, {"FAKE_RUN_ID": bad}):
322
+ r = v10_tools.seal_diff(self.repo, "add a line", _ok)
323
+ self.assertIn("error", r, bad)
324
+ self.assertIn("run_id", r["error"], bad)
325
+ self.assertFalse(os.path.exists(os.path.join(self.repo, ".loki", "runs", "escaped")))
326
+ self.assertFalse(os.path.exists(os.path.join(self.repo, "escaped")))
327
+
328
+ def test_sealed_commit_lands_under_a_non_branch_ref_and_branches_are_untouched(self):
329
+ self._edit()
330
+ branches = _git(self.repo, "branch", "--list")
331
+ r = v10_tools.seal_diff(self.repo, "add a line", _ok)
332
+ self.assertEqual(r["sealed_ref"], "refs/loki/seal/e10-fake")
333
+ # the fake seals at the clone HEAD, which is the user's HEAD; the ref must resolve in the USER repo
334
+ self.assertEqual(_git(self.repo, "rev-parse", r["sealed_ref"]), _git(self.repo, "rev-parse", "HEAD"))
335
+ self.assertEqual(_git(self.repo, "branch", "--list"), branches)
336
+ self.assertIn(".loki/", r["note"])
337
+
338
+ def test_result_does_not_call_the_verdict_string_a_checked_signature(self):
339
+ self._edit()
340
+ r = v10_tools.seal_diff(self.repo, "add a line", _ok)
341
+ self.assertNotIn("verified", r)
342
+ self.assertIn("verdict_is_verified", r)
343
+
344
+ def test_failed_wall_is_not_verified(self):
345
+ self._edit()
346
+ with mock.patch.dict(os.environ, {"FAKE_VERDICT": "FAILED"}):
347
+ r = v10_tools.seal_diff(self.repo, "add a line", _ok)
348
+ self.assertEqual((r["verdict"], r["verdict_is_verified"]), ("FAILED", False))
349
+
350
+ def test_rejects_bad_input_and_empty_diff(self):
351
+ self.assertIn("error", v10_tools.seal_diff(self.repo, "", _ok))
352
+ self.assertIn("error", v10_tools.seal_diff(self.repo, "--help", _ok))
353
+ self.assertIn("error", v10_tools.seal_diff(self.repo, "x", _deny))
354
+ r = v10_tools.seal_diff(self.repo, "x", _ok)
355
+ self.assertIn("no changes to seal", r["error"])
356
+
357
+ def test_scratch_dir_removed(self):
358
+ self._edit()
359
+ made = []
360
+ real = tempfile.mkdtemp
361
+ def spy(*a, **k):
362
+ d = real(*a, **k)
363
+ made.append(d)
364
+ return d
365
+ with mock.patch("tempfile.mkdtemp", spy):
366
+ v10_tools.seal_diff(self.repo, "add a line", _ok)
367
+ self.assertEqual(len(made), 1)
368
+ self.assertFalse(os.path.exists(made[0]))
369
+
370
+
371
+ _E2E = os.path.join(_REPO_ROOT, "loki-ts", "tests", "engine10", "fixtures", "e2e")
372
+
373
+
374
+ @unittest.skipUnless(shutil.which("bun") and shutil.which("git"), "needs bun and git")
375
+ class RealEngineSealDiffTests(unittest.TestCase):
376
+ """FC-ENGINE-ROUTE-UNMOCKED: seal_diff through the REAL bin/loki under v10_tools._env() (LOKI_ENGINE=v10), so
377
+ autonomy/route.sh must accept --seal-existing. Only the claude CLI is a stub (no billed call)."""
378
+
379
+ def setUp(self):
380
+ self.tmp = tempfile.mkdtemp(prefix="loki-test-real-seal-")
381
+ self.addCleanup(shutil.rmtree, self.tmp, True)
382
+ self.repo = os.path.join(self.tmp, "repo")
383
+ shutil.copytree(os.path.join(_E2E, "repo"), self.repo)
384
+ _git(self.repo, "init", "-q", "-b", "main")
385
+ _git(self.repo, "config", "user.name", "t")
386
+ _git(self.repo, "config", "user.email", "t@example.invalid")
387
+ _git(self.repo, "add", "calc.ts", "calc.test.ts", "bunfig.toml")
388
+ _git(self.repo, "commit", "-q", "-m", "base")
389
+ with open(os.path.join(self.repo, "calc.ts"), "a") as f:
390
+ f.write("\nexport function multiply(a: number, b: number): number {\n return a * b;\n}\n")
391
+ patches = [
392
+ mock.patch.object(v10_tools, "LOKI_BIN", os.path.join(_REPO_ROOT, "bin", "loki")),
393
+ mock.patch.dict(os.environ, {
394
+ "LOKI_E10_INVOKER": "cli", "LOKI_CLAUDE_CLI": os.path.join(_E2E, "bin", "claude"),
395
+ "PATH": os.path.join(_E2E, "bin") + os.pathsep + os.environ.get("PATH", ""),
396
+ "E2E_STUB_MODE": "wallcolor", "LOKI_NO_BROWSER": "1",
397
+ "LOKI_RECEIPT_SIGNING_KEY_FILE": os.path.join(self.tmp, "k.pem"),
398
+ "LOKI_TS_ENTRY": os.path.join(_REPO_ROOT, "loki-ts", "src", "cli.ts")}),
399
+ ]
400
+ for p in patches:
401
+ p.start()
402
+ self.addCleanup(p.stop)
403
+ for k in ("LOKI_LEGACY_BASH", "LOKI_MODEL_OVERRIDE", "LOKI_RECEIPT_SIGNING_KEY", "LOKI_ENGINE", "LOKI_E10_PLAN"): # no plan mode: the shipped path
404
+ os.environ.pop(k, None)
405
+
406
+ def _loki(self, *args):
407
+ return subprocess.run([v10_tools.LOKI_BIN, *args], cwd=self.repo, env=v10_tools._env(), capture_output=True,
408
+ text=True, stdin=subprocess.DEVNULL, timeout=120)
409
+
410
+ def _seal(self):
411
+ r = v10_tools.seal_diff(self.repo, "add a multiply(a, b) function to calc.ts", _ok)
412
+ self.assertNotIn("error", r, r)
413
+ return r
414
+
415
+ def test_route_accepts_seal_existing_and_receipt_names_no_implementing_model(self):
416
+ r = self._seal()
417
+ self.assertEqual(r["verdict"], "VERIFIED")
418
+ receipt = json.load(open(r["receipt_path"]))
419
+ self.assertEqual(receipt["implement"]["exit"], "external")
420
+ self.assertEqual(receipt["model"], "external (no Loki implement session)")
421
+ self.assertIn("Wall author not filesystem-isolated from the external diff", r["not_proven"])
422
+
423
+ def test_wrong_external_body_never_seals_verified_on_the_shipped_path(self):
424
+ """FC-EXTERNAL-SEAL-WITHOUT-WALL-VERIFIED: no LOKI_E10_PLAN, wrong multiply body, default env through the tool."""
425
+ calc = os.path.join(self.repo, "calc.ts")
426
+ _git(self.repo, "checkout", "--", "calc.ts")
427
+ with open(calc, "a") as f:
428
+ f.write("\nexport function multiply(a: number, b: number): number {\n return a + b + 1;\n}\n")
429
+ self.assertNotIn("LOKI_E10_PLAN", v10_tools._env())
430
+ r = v10_tools.seal_diff(self.repo, "add a multiply(a, b) function to calc.ts", _ok)
431
+ self.assertNotIn("error", r, r)
432
+ self.assertNotEqual(r["verdict"], "VERIFIED", r)
433
+ self.assertFalse(r["verdict_is_verified"])
434
+
435
+ def test_user_repo_verifies_strictly_with_no_drift_override(self):
436
+ before = (_git(self.repo, "rev-parse", "HEAD"), _git(self.repo, "diff", "--cached"), _git(self.repo, "branch", "--list"))
437
+ r = self._seal()
438
+ self.assertEqual(r["sealed_ref"], "refs/loki/seal/" + r["run_id"])
439
+ self.assertEqual((_git(self.repo, "rev-parse", "HEAD"), _git(self.repo, "diff", "--cached"), _git(self.repo, "branch", "--list")), before)
440
+ env = v10_tools._env()
441
+ self.assertNotIn("LOKI_VERIFY_ALLOW_HEAD_DRIFT", env)
442
+ # Before the user commits, HEAD is still the base and the sealed path is uncommitted: verify honestly says UNCHECKED.
443
+ pre = self._loki("verify", r["run_id"])
444
+ self.assertEqual(pre.returncode, 2, pre.stdout + pre.stderr)
445
+ self.assertIn("head drift", pre.stdout)
446
+ # After the user commits the sealed change, the fetched sealed head makes the drift computable: strict VERIFIED.
447
+ _git(self.repo, "add", "calc.ts")
448
+ _git(self.repo, "commit", "-q", "-m", "user commit of the sealed change")
449
+ p = self._loki("verify", r["run_id"])
450
+ self.assertEqual(p.returncode, 0, p.stdout + p.stderr)
451
+ self.assertIn("verdict: VERIFIED", p.stdout)
452
+ # Negative control: without the fetched ref the same verify cannot compute the drift and is UNCHECKED again.
453
+ _git(self.repo, "update-ref", "-d", r["sealed_ref"])
454
+ _git(self.repo, "reflog", "expire", "--expire=now", "--all")
455
+ _git(self.repo, "gc", "-q", "--prune=now")
456
+ gone = self._loki("verify", r["run_id"])
457
+ self.assertEqual(gone.returncode, 2, gone.stdout + gone.stderr)
458
+ self.assertIn("head drift", gone.stdout)
package/mcp/v10_tools.py CHANGED
@@ -9,15 +9,19 @@ validator before use.
9
9
  import json
10
10
  import os
11
11
  import re
12
+ import shutil
12
13
  import subprocess
14
+ import tempfile
13
15
  import time
14
16
  from pathlib import Path
15
17
  from typing import Callable, Dict, List, Optional
16
18
 
17
19
  LOKI_BIN = str(Path(__file__).resolve().parent.parent / "bin" / "loki")
18
20
  RUNS_REL = os.path.join(".loki", "runs")
21
+ RUN_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
19
22
  V10_RUN_ID_WAIT_S = 2.0
20
23
  V10_VERIFY_TIMEOUT_S = 120
24
+ SEAL_DIFF_TIMEOUT_S = int(os.environ.get("LOKI_SEAL_DIFF_TIMEOUT_S", "1800"))
21
25
  _TERMINAL = {"stage.completed", "stage.failed", "stage.skipped"}
22
26
 
23
27
 
@@ -195,6 +199,85 @@ def v10_verify(receipt_path: str, repo_path: str, validate: Callable[[str], str]
195
199
  return {"exit_code": p.returncode, "verified": p.returncode == 0, "output": out[-2000:]}
196
200
 
197
201
 
202
+ def _git(cwd: str, *args: str, env: Optional[Dict[str, str]] = None) -> subprocess.CompletedProcess:
203
+ return subprocess.run(["git", *args], cwd=cwd, env=env, stdin=subprocess.DEVNULL,
204
+ capture_output=True, timeout=120)
205
+
206
+
207
+ def seal_diff(repo_path: str, intent: str, validate: Callable[[str], str]) -> dict:
208
+ """Seal changes already made in repo_path (CW-12): the existing engine runs on a scratch clone of HEAD with the
209
+ working-tree diff handed to `--seal-existing`, so the Wall is authored from the intent alone and the diff only
210
+ reaches the tree at implement. The user's tree, index and branch are never touched; only the run dir is copied back."""
211
+ if not isinstance(intent, str) or not intent.strip() or intent.startswith("-") or "\x00" in intent:
212
+ return {"error": "invalid intent: must be a non-empty description that does not start with '-'"}
213
+ try:
214
+ repo = validate(repo_path)
215
+ except Exception as e:
216
+ return {"error": str(e)}
217
+ if not os.path.isdir(repo):
218
+ return {"error": f"repo_path is not a directory: {repo_path}"}
219
+ work = tempfile.mkdtemp(prefix="loki-seal-")
220
+ try:
221
+ if _git(repo, "rev-parse", "--verify", "-q", "HEAD").returncode != 0:
222
+ return {"error": "repo_path is not a git repository with a commit to diff against"}
223
+ # Snapshot the working tree (tracked edits plus new files) with a private index so the user's index is untouched.
224
+ idx_env = dict(os.environ, GIT_INDEX_FILE=os.path.join(work, "index"))
225
+ for step in (("read-tree", "HEAD"), ("add", "-A")):
226
+ r = _git(repo, *step, env=idx_env)
227
+ if r.returncode != 0:
228
+ return {"error": f"could not snapshot the working tree: {r.stderr.decode('utf-8', 'replace').strip()[-300:]}"}
229
+ d = _git(repo, "diff", "--cached", "--binary", "HEAD", env=idx_env)
230
+ if d.returncode != 0 or not d.stdout.strip():
231
+ return {"error": "no changes to seal: the working tree matches HEAD"}
232
+ patch = os.path.join(work, "external.patch")
233
+ with open(patch, "wb") as f:
234
+ f.write(d.stdout)
235
+ clone = os.path.join(work, "clone")
236
+ c = _git(work, "clone", "--quiet", "--no-hardlinks", repo, clone)
237
+ if c.returncode != 0:
238
+ return {"error": f"could not clone the repository: {c.stderr.decode('utf-8', 'replace').strip()[-300:]}"}
239
+ try:
240
+ p = subprocess.run([LOKI_BIN, intent, "--seal-existing", patch], cwd=clone, env=_env(), stdin=subprocess.DEVNULL,
241
+ capture_output=True, text=True, timeout=SEAL_DIFF_TIMEOUT_S)
242
+ except subprocess.TimeoutExpired:
243
+ return {"error": f"seal timed out after {SEAL_DIFF_TIMEOUT_S}s"}
244
+ except OSError as e:
245
+ return {"error": f"could not run loki: {e}"}
246
+ out = ((p.stdout or "") + (p.stderr or "")).strip()
247
+ try:
248
+ with open(os.path.join(clone, ".loki", "engine.json"), "r", encoding="utf-8") as f:
249
+ run_id = json.load(f)["run_id"]
250
+ if not isinstance(run_id, str) or not RUN_ID_RE.match(run_id) or ".." in run_id:
251
+ return {"error": "refusing a run_id that is not a plain run id"} # the clone is engine-written but agent-writable: never join it into a path unchecked
252
+ src = os.path.join(clone, RUNS_REL, run_id)
253
+ with open(os.path.join(src, "receipt.json"), "r", encoding="utf-8") as f:
254
+ receipt = json.load(f)
255
+ except (OSError, ValueError, KeyError):
256
+ return {"error": f"loki exited {p.returncode} without a receipt. Output tail:\n{out[-1500:]}"}
257
+ dest = os.path.join(repo, RUNS_REL, run_id)
258
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
259
+ shutil.copytree(src, dest, dirs_exist_ok=True)
260
+ # B4: bring the sealed commit into the user repo under a non-branch ref so `loki verify <id>` resolves the sealed head
261
+ # without LOKI_VERIFY_ALLOW_HEAD_DRIFT. Never a branch: HEAD, the index and tracked files stay untouched.
262
+ ref = f"refs/loki/seal/{run_id}"
263
+ sealed_ref = None
264
+ head = receipt.get("head_sha")
265
+ if isinstance(head, str) and re.fullmatch(r"[0-9a-f]{40,64}", head):
266
+ if _git(clone, "update-ref", ref, head).returncode == 0 and \
267
+ _git(repo, "fetch", "--quiet", "--no-tags", clone, f"{ref}:{ref}").returncode == 0:
268
+ sealed_ref = ref
269
+ verdict = receipt.get("verdict")
270
+ return {"run_id": run_id, "verdict": verdict,
271
+ "verdict_is_verified": verdict == "VERIFIED", # the receipt's own verdict string; no signature is checked here, run loki_v10_verify for that
272
+ "receipt_path": os.path.join(dest, "receipt.json"), "sealed_ref": sealed_ref,
273
+ "not_proven": receipt.get("not_proven", []), "exit_code": p.returncode,
274
+ "note": "the new untracked .loki/ directory in the repo is expected (loki start creates it the same way); HEAD, the index and tracked files are unchanged"}
275
+ except subprocess.TimeoutExpired:
276
+ return {"error": "git timed out"}
277
+ finally:
278
+ shutil.rmtree(work, ignore_errors=True)
279
+
280
+
198
281
  def _cwd_validator(p: str) -> str:
199
282
  """Resolve p and require it to sit inside the current working directory (the MCP server's, as for the SDK path)."""
200
283
  root = os.path.realpath(os.getcwd())
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": "11.66.2",
4
+ "version": "11.67.1",
5
5
  "description": "An autonomous software factory that knows what it is supposed to deliver, and proves it did.",
6
6
  "keywords": [
7
7
  "agent",
@@ -160,5 +160,5 @@
160
160
  "dependencies": {
161
161
  "@resvg/resvg-wasm": "^2.6.2"
162
162
  },
163
- "gitHead": "a0b492c2b527118fa75885b90d4a40eeb6c57d1c"
163
+ "gitHead": "6ff9a3206d5e346bf72c0786a44fe02575091d93"
164
164
  }