loki-mode 9.18.4 → 9.20.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.
Files changed (84) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/lib/deadline.py +21 -1
  4. package/autonomy/lib/outcome_contract.py +128 -0
  5. package/autonomy/lib/proof-generator.py +45 -0
  6. package/autonomy/lib/proof-verify.py +53 -0
  7. package/autonomy/loki +980 -24
  8. package/autonomy/receipt_jwt.py +308 -0
  9. package/autonomy/run.sh +30 -0
  10. package/autonomy/trigger-server.py +186 -1
  11. package/dashboard/__init__.py +1 -1
  12. package/dashboard/server.py +83 -0
  13. package/dashboard/static/index.html +144 -0
  14. package/docs/COMPETITIVE-SCORECARD.md +76 -0
  15. package/docs/PROOF-PASSPORT.md +29 -0
  16. package/docs/SIGNED-RECEIPTS.md +111 -0
  17. package/docs/metrics.md +32 -0
  18. package/events/emit.sh +31 -0
  19. package/loki-ts/dist/loki.js +2 -2
  20. package/mcp/__init__.py +1 -1
  21. package/package.json +1 -1
  22. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  23. package/tools/proof-passport.py +175 -0
  24. package/web-app/dist/assets/{AdminPage-CcCJ0Sjt.js → AdminPage-DNGNuAge.js} +1 -1
  25. package/web-app/dist/assets/{Avatar-DK8kmayw.js → Avatar-CqAveJaT.js} +1 -1
  26. package/web-app/dist/assets/{Badge-4uAWnemi.js → Badge-BtMejA0Z.js} +1 -1
  27. package/web-app/dist/assets/{Button-BBMk33tk.js → Button-CikxmqSs.js} +1 -1
  28. package/web-app/dist/assets/{ComparePage-bt9rwvST.js → ComparePage-BB8DJBWR.js} +1 -1
  29. package/web-app/dist/assets/EvidenceReceiptPanel-6oquKEjd.js +1 -0
  30. package/web-app/dist/assets/{GitHubIssuesPanel-WDbH47UM.js → GitHubIssuesPanel-5XLYWzWH.js} +1 -1
  31. package/web-app/dist/assets/{GitHubPRsPanel-C2CiYtTx.js → GitHubPRsPanel-DUfmFYeu.js} +1 -1
  32. package/web-app/dist/assets/{HomePage-BQk-MUjn.js → HomePage-DZ4MmOdH.js} +5 -5
  33. package/web-app/dist/assets/{LoginPage-DMOZVGGL.js → LoginPage-VMzI6ROD.js} +1 -1
  34. package/web-app/dist/assets/{MagicPage-Bzp2Nt1z.js → MagicPage-CFbX5gWg.js} +1 -1
  35. package/web-app/dist/assets/{MetricsPage-C39JVdsw.js → MetricsPage-DbL6xmRQ.js} +2 -2
  36. package/web-app/dist/assets/{NotFoundPage-6vT_U9UL.js → NotFoundPage-BUrjXPAR.js} +1 -1
  37. package/web-app/dist/assets/{ProjectPage-BfFcZp-E.js → ProjectPage-iP6dJihg.js} +3 -3
  38. package/web-app/dist/assets/{ProjectsPage-CPMBf8Wt.js → ProjectsPage-BibN9bHl.js} +1 -1
  39. package/web-app/dist/assets/{SettingsPage-BnNN6ETl.js → SettingsPage-C0WJp1hW.js} +1 -1
  40. package/web-app/dist/assets/{ShowcasePage-WDrMf-cx.js → ShowcasePage-DUv0Njp8.js} +1 -1
  41. package/web-app/dist/assets/{SystemSettingsPage-DX4jb2e8.js → SystemSettingsPage-BIaCB0Vn.js} +1 -1
  42. package/web-app/dist/assets/{TeamsPage-BCfqcXzu.js → TeamsPage-BE0szv9r.js} +1 -1
  43. package/web-app/dist/assets/{TemplatesPage-CZvmimDj.js → TemplatesPage-BzkA9kat.js} +1 -1
  44. package/web-app/dist/assets/{TerminalOutput-BlRqFwWV.js → TerminalOutput-DBrdT_Fp.js} +1 -1
  45. package/web-app/dist/assets/{activity-CacZsUyr.js → activity-AbfFz38r.js} +1 -1
  46. package/web-app/dist/assets/{bell-DK2qtHnk.js → bell-DF6Z6y5U.js} +1 -1
  47. package/web-app/dist/assets/{bot-CkcUtHad.js → bot-DQ4np_3n.js} +1 -1
  48. package/web-app/dist/assets/{check-CbCPjX3M.js → check-RWuguCwE.js} +1 -1
  49. package/web-app/dist/assets/{chevron-left-5NUKWw3i.js → chevron-left-VpbQPOkC.js} +1 -1
  50. package/web-app/dist/assets/{circle-alert-S7uFoxC2.js → circle-alert-DwT82lJx.js} +1 -1
  51. package/web-app/dist/assets/{clock-CaQRrIrs.js → clock-BBlUa93G.js} +1 -1
  52. package/web-app/dist/assets/{cloud-DBAX6c0r.js → cloud-Dgbz3VCA.js} +1 -1
  53. package/web-app/dist/assets/{code-xml-De5-EXv3.js → code-xml-r6qqION5.js} +1 -1
  54. package/web-app/dist/assets/{copy-CUkT6k1v.js → copy-DMHj52B8.js} +1 -1
  55. package/web-app/dist/assets/{database-BAWf1Gwt.js → database-uJqHSHR1.js} +1 -1
  56. package/web-app/dist/assets/{dollar-sign-Ji8zk86R.js → dollar-sign-BZagljek.js} +1 -1
  57. package/web-app/dist/assets/{file-code-corner-ChtXoBwS.js → file-code-corner-DMopmhu0.js} +1 -1
  58. package/web-app/dist/assets/{file-plus-bFa37P76.js → file-plus-DU_hT_3w.js} +1 -1
  59. package/web-app/dist/assets/{folder-open-DhXpXscO.js → folder-open-BP5TtQAW.js} +1 -1
  60. package/web-app/dist/assets/{git-commit-horizontal-DVPeDQ3j.js → git-commit-horizontal-zFk_IobS.js} +1 -1
  61. package/web-app/dist/assets/{globe-BPZgPeeu.js → globe-mthCRtfp.js} +1 -1
  62. package/web-app/dist/assets/{hammer-jLCaujYH.js → hammer-BWaLBvSA.js} +1 -1
  63. package/web-app/dist/assets/{index-B-0iHBPO.js → index-BXA3UT6P.js} +3 -3
  64. package/web-app/dist/assets/{index-BfZSDej1.css → index-DgpUuqwR.css} +1 -1
  65. package/web-app/dist/assets/{layers-B1vsrsFW.js → layers-Q1LXV5fy.js} +1 -1
  66. package/web-app/dist/assets/{lightbulb-C-uLoq9Y.js → lightbulb-vRRNJ3dB.js} +1 -1
  67. package/web-app/dist/assets/{loader-circle-JTfD-ZuM.js → loader-circle-KgjeHEcV.js} +1 -1
  68. package/web-app/dist/assets/{lock-G9rxD4gZ.js → lock-BerKqno7.js} +1 -1
  69. package/web-app/dist/assets/{mail-BJ0PTN_V.js → mail-XAxTbprX.js} +1 -1
  70. package/web-app/dist/assets/{package-CXClfLOO.js → package-DLPKWAvW.js} +1 -1
  71. package/web-app/dist/assets/{plus-EoL5OCB7.js → plus-NMxxwHMn.js} +1 -1
  72. package/web-app/dist/assets/{refresh-cw-BjREUnVq.js → refresh-cw-Dt42kYtM.js} +1 -1
  73. package/web-app/dist/assets/{rotate-ccw-DahWX07H.js → rotate-ccw-BCR4Tp9u.js} +1 -1
  74. package/web-app/dist/assets/{save-Dek3gCn1.js → save-sR4MJd42.js} +1 -1
  75. package/web-app/dist/assets/{server-D6V1BAia.js → server-SXkchokx.js} +1 -1
  76. package/web-app/dist/assets/{shield-alert-BtTK5Sxb.js → shield-alert-Bvgp6_Dz.js} +1 -1
  77. package/web-app/dist/assets/{trash-2-BT5o_g0r.js → trash-2-D0IGudH7.js} +1 -1
  78. package/web-app/dist/assets/{trending-down-D4Jk7KF3.js → trending-down-BbFFBq-v.js} +1 -1
  79. package/web-app/dist/assets/{trending-up-EQFTzhEo.js → trending-up-DDrJKL5i.js} +1 -1
  80. package/web-app/dist/assets/{upload-JfI5lCSE.js → upload-C54rjN8K.js} +1 -1
  81. package/web-app/dist/assets/{usePolling-BnhPUuGd.js → usePolling-D7fw4YSM.js} +1 -1
  82. package/web-app/dist/assets/{user-DSUiUYtj.js → user-Cg7fbzdh.js} +1 -1
  83. package/web-app/dist/index.html +2 -2
  84. package/web-app/server.py +78 -0
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 v9.18.4
6
+ # Loki Mode v9.20.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
470
470
 
471
471
  ---
472
472
 
473
- **v9.18.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.20.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.18.4
1
+ 9.20.0
@@ -281,7 +281,16 @@ def _process_has_lineage_token(pid: int, token: str) -> bool:
281
281
  if sys.platform.startswith("linux"):
282
282
  try:
283
283
  with open(f"/proc/{pid}/environ", "rb") as handle:
284
- return needle in handle.read().split(b"\0")
284
+ payload = handle.read()
285
+ if not payload:
286
+ # A zero-length read raises NOTHING. The kernel empties
287
+ # /proc/<pid>/environ as the process is reaped, so a fast
288
+ # runner yields b"" -- which splits to [b""], does not contain
289
+ # the needle, and returns "no marker" for a process that
290
+ # carried one. Indistinguishable from the FileNotFoundError
291
+ # case below and must be answered the same way.
292
+ raise _LineageUnknown(pid)
293
+ return needle in payload.split(b"\0")
285
294
  except FileNotFoundError:
286
295
  # The process is already gone. /proc/<pid>/environ disappears the
287
296
  # moment a child is reaped, so a short-lived runner that exits
@@ -295,7 +304,18 @@ def _process_has_lineage_token(pid: int, token: str) -> bool:
295
304
  # macOS never hit this: it has the pipe-handle fallback, so the
296
305
  # bug was Linux-only and invisible on a developer Mac.
297
306
  raise _LineageUnknown(pid) from None
307
+ except ProcessLookupError:
308
+ # ESRCH: the pid was reaped between opening and reading. Same fact
309
+ # as ENOENT above -- the process is gone, so its marker is UNKNOWN,
310
+ # not absent. Listed BEFORE the OSError clause because it is a
311
+ # subclass and would otherwise be swallowed into "no marker",
312
+ # which fails the lineage guard and reports 127 for a process that
313
+ # launched and completed.
314
+ raise _LineageUnknown(pid) from None
298
315
  except OSError:
316
+ # Anything else (EACCES on a hardened /proc, EIO) is a genuine
317
+ # read failure rather than evidence of a completed child, so it
318
+ # stays fail-closed: no marker.
299
319
  return False
300
320
  return False
301
321
 
@@ -0,0 +1,128 @@
1
+ """Pure-stdlib validation and deterministic identity for Outcome Contract v0.1."""
2
+
3
+ import hashlib
4
+ import json
5
+
6
+ SCHEMA = "autonomi-outcome-contract/v0.1"
7
+ KINDS = {"human", "agent", "service", "organization"}
8
+ TOP_KEYS = {"schema", "id", "intent", "acceptance", "executor", "verifier",
9
+ "constraints", "evidence", "risk", "budget", "deployment",
10
+ "rollback", "dispute"}
11
+
12
+
13
+ class ContractValidationError(ValueError):
14
+ def __init__(self, path, message):
15
+ self.path = path
16
+ self.message = message
17
+ super().__init__("%s: %s" % (path, message))
18
+
19
+
20
+ def _fail(path, message):
21
+ raise ContractValidationError(path, message)
22
+
23
+
24
+ def _text(value, path):
25
+ if not isinstance(value, str) or not value.strip():
26
+ _fail(path, "must be a non-empty string")
27
+
28
+
29
+ def _identity(value, path):
30
+ if not isinstance(value, dict):
31
+ _fail(path, "must be an object")
32
+ extra = sorted(set(value) - {"id", "kind"})
33
+ if extra:
34
+ _fail(path + "." + extra[0], "unknown property")
35
+ for key in ("id", "kind"):
36
+ if key not in value:
37
+ _fail(path + "." + key, "is required")
38
+ _text(value["id"], path + ".id")
39
+ if value["kind"] not in KINDS:
40
+ _fail(path + ".kind", "must be one of: %s" % ", ".join(sorted(KINDS)))
41
+
42
+
43
+ def _object(value, path, required, allowed):
44
+ if not isinstance(value, dict):
45
+ _fail(path, "must be an object")
46
+ extra = sorted(set(value) - set(allowed))
47
+ if extra:
48
+ _fail(path + "." + extra[0], "unknown property")
49
+ for key in required:
50
+ if key not in value:
51
+ _fail(path + "." + key, "is required")
52
+
53
+
54
+ def validate(contract):
55
+ if not isinstance(contract, dict):
56
+ _fail("$", "must be an object")
57
+ extra = sorted(set(contract) - TOP_KEYS)
58
+ if extra:
59
+ _fail("$." + extra[0], "unknown property")
60
+ for key in ("schema", "id", "intent", "acceptance", "executor", "verifier"):
61
+ if key not in contract:
62
+ _fail("$." + key, "is required")
63
+ if contract["schema"] != SCHEMA:
64
+ _fail("$.schema", "must equal %s" % SCHEMA)
65
+ _text(contract["id"], "$.id")
66
+ _text(contract["intent"], "$.intent")
67
+ acceptance = contract["acceptance"]
68
+ if not isinstance(acceptance, list) or not acceptance:
69
+ _fail("$.acceptance", "must be a non-empty array")
70
+ for index, item in enumerate(acceptance):
71
+ _text(item, "$.acceptance[%d]" % index)
72
+ _identity(contract["executor"], "$.executor")
73
+ _identity(contract["verifier"], "$.verifier")
74
+ if contract["executor"]["id"] == contract["verifier"]["id"]:
75
+ _fail("$.verifier.id", "must differ from executor.id")
76
+ if "constraints" in contract and not isinstance(contract["constraints"], dict):
77
+ _fail("$.constraints", "must be an object")
78
+ if "risk" in contract:
79
+ value = contract["risk"]
80
+ _object(value, "$.risk", ("level",), ("level", "notes"))
81
+ if value["level"] not in {"low", "medium", "high", "critical"}:
82
+ _fail("$.risk.level", "must be one of: low, medium, high, critical")
83
+ if "notes" in value:
84
+ _text(value["notes"], "$.risk.notes")
85
+ if "budget" in contract:
86
+ value = contract["budget"]
87
+ _object(value, "$.budget", ("max_amount", "currency"),
88
+ ("max_amount", "currency"))
89
+ if isinstance(value["max_amount"], bool) or not isinstance(
90
+ value["max_amount"], (int, float)) or value["max_amount"] < 0:
91
+ _fail("$.budget.max_amount", "must be a non-negative number")
92
+ _text(value["currency"], "$.budget.currency")
93
+ if "deployment" in contract:
94
+ value = contract["deployment"]
95
+ _object(value, "$.deployment", ("environment", "approval_required"),
96
+ ("environment", "approval_required"))
97
+ _text(value["environment"], "$.deployment.environment")
98
+ if not isinstance(value["approval_required"], bool):
99
+ _fail("$.deployment.approval_required", "must be a boolean")
100
+ if "rollback" in contract:
101
+ value = contract["rollback"]
102
+ _object(value, "$.rollback", ("required", "strategy"),
103
+ ("required", "strategy"))
104
+ if not isinstance(value["required"], bool):
105
+ _fail("$.rollback.required", "must be a boolean")
106
+ _text(value["strategy"], "$.rollback.strategy")
107
+ if "dispute" in contract:
108
+ value = contract["dispute"]
109
+ _object(value, "$.dispute", ("process",), ("process", "arbiter"))
110
+ _text(value["process"], "$.dispute.process")
111
+ if "arbiter" in value:
112
+ _identity(value["arbiter"], "$.dispute.arbiter")
113
+ if "evidence" in contract:
114
+ if not isinstance(contract["evidence"], list):
115
+ _fail("$.evidence", "must be an array")
116
+ for index, item in enumerate(contract["evidence"]):
117
+ _text(item, "$.evidence[%d]" % index)
118
+ return contract
119
+
120
+
121
+ def canonicalize(contract):
122
+ validate(contract)
123
+ return json.dumps(contract, sort_keys=True, separators=(",", ":"),
124
+ ensure_ascii=False)
125
+
126
+
127
+ def digest(contract):
128
+ return hashlib.sha256(canonicalize(contract).encode("utf-8")).hexdigest()
@@ -1867,6 +1867,51 @@ def generate(args):
1867
1867
  if sig:
1868
1868
  verification["gpg_signature"] = sig
1869
1869
 
1870
+ # Optional Ed25519 attestation over the SAME digest, for the case gpg
1871
+ # cannot serve: a receipt checked by someone who does not hold the signing
1872
+ # key. gpg proves provenance only to a verifier who already imported the
1873
+ # public key, which a customer, auditor or CI system handed a proof.json
1874
+ # generally has not. An attestation is checkable against a published JWKS
1875
+ # (or a jwks.json file, offline) with no key exchange at all.
1876
+ #
1877
+ # Binds the DIGEST, not the body, so it attests to exactly the bytes a
1878
+ # verifier independently recomputes. Written INSIDE `verification`, which
1879
+ # the hash excludes -- anywhere else would change the hashed bytes and make
1880
+ # every honest receipt read as tampered the moment it was signed.
1881
+ #
1882
+ # Same default-OFF discipline as gpg above: absent the key, no field, and
1883
+ # the bytes stay identical to an unattested proof. Best-effort for the same
1884
+ # reason -- a signing failure must not cost the user their receipt.
1885
+ # This env check is an EARLY EXIT, not the control that enforces default-off.
1886
+ # Mutation testing showed that removing it changes nothing observable:
1887
+ # load_signing_key() returns (None, "") when neither variable is set, so the
1888
+ # block below cannot attest anyway. It is kept because it skips an import
1889
+ # and a try/except on the overwhelmingly common unconfigured path -- stated
1890
+ # here rather than left to imply a guarantee it does not provide.
1891
+ att_key = (os.environ.get("LOKI_RECEIPT_SIGNING_KEY_FILE", "").strip()
1892
+ or os.environ.get("LOKI_RECEIPT_SIGNING_KEY", "").strip())
1893
+ if att_key:
1894
+ try:
1895
+ sys.path.insert(0, os.path.dirname(os.path.dirname(
1896
+ os.path.abspath(__file__))))
1897
+ from receipt_jwt import load_signing_key, sign_attestation
1898
+ _priv, _kid = load_signing_key()
1899
+ if _priv is not None:
1900
+ _tok = sign_attestation(
1901
+ _priv, _kid,
1902
+ job_id=str(redacted.get("run_id") or ""),
1903
+ run_id=str(redacted.get("run_id") or ""),
1904
+ receipt_hash=digest,
1905
+ )
1906
+ if _tok:
1907
+ verification["attestation"] = _tok
1908
+ verification["attestation_kid"] = _kid
1909
+ except Exception:
1910
+ # Swallowed deliberately, matching the gpg path: an unattested
1911
+ # receipt is a valid state with an existing verdict. Losing the
1912
+ # receipt entirely because signing failed would be strictly worse.
1913
+ pass
1914
+
1870
1915
  redacted["verification"] = verification
1871
1916
 
1872
1917
  # Determine output dir.
@@ -76,6 +76,46 @@ if _HERE not in sys.path:
76
76
  from tree_digest import compute_tree_digest
77
77
  from workspace_diff import collect_workspace_diff
78
78
 
79
+ # Receipt schema major this verifier understands. proof-generator.py writes
80
+ # schema_version ("1.1" today) and NOTHING read it -- a version stamped on
81
+ # every receipt and enforced nowhere, so a future incompatible receipt would
82
+ # have been verified against rules written for a different shape and reported
83
+ # as a clean pass.
84
+ #
85
+ # FAIL CLOSED on absent or unknown: a receipt we cannot place is not a receipt
86
+ # we can verify, and "cannot tell" must never render as "verified".
87
+ #
88
+ # ACCEPT EVERY MINOR of the supported major -- that is the legacy compatibility
89
+ # guarantee, and it is what keeps this from being a breaking change. Minor bumps
90
+ # are additive by convention, so 1.0 and today's 1.1 both verify; only a MAJOR
91
+ # bump (2.x) means the shape changed enough that these rules no longer apply.
92
+ SUPPORTED_SCHEMA_MAJOR = 1
93
+
94
+
95
+ def check_schema_version(proof):
96
+ """Return None when the receipt's schema is supported, else a reason string.
97
+
98
+ Returns a reason rather than raising so the caller folds it into the normal
99
+ reason/reasons/ok contract, which every consumer (loki proof verify, the
100
+ remote receipt path, the dashboard) already handles.
101
+ """
102
+ raw = proof.get("schema_version")
103
+ if raw is None:
104
+ return ("receipt has no schema_version, so the rules to verify it "
105
+ "cannot be established (expected major %d)" % SUPPORTED_SCHEMA_MAJOR)
106
+ text = str(raw).strip()
107
+ major_text = text.split(".", 1)[0]
108
+ try:
109
+ major = int(major_text)
110
+ except (TypeError, ValueError):
111
+ return ("receipt schema_version %r is not a parseable version, so the "
112
+ "rules to verify it cannot be established" % raw)
113
+ if major != SUPPORTED_SCHEMA_MAJOR:
114
+ return ("receipt schema_version %s is major %d; this verifier "
115
+ "understands major %d only, so its checks may not apply"
116
+ % (text, major, SUPPORTED_SCHEMA_MAJOR))
117
+ return None
118
+
79
119
 
80
120
  # ---------------------------------------------------------------------------
81
121
  # canonicalization (MUST match proof-generator._canonical exactly)
@@ -863,11 +903,24 @@ def verify(proof_path, repo_dir="."):
863
903
  "working tree changed after the receipt was generated" % (
864
904
  recorded_tree, current_tree))
865
905
 
906
+ # ----- schema version --------------------------------------------------
907
+ # Folded into the verdict rather than raised, so every existing caller
908
+ # inherits it through reason/reasons/ok with no signature change. ANDed in
909
+ # below: an unsupported schema cannot be rescued by a passing hash, because
910
+ # the hash only proves the bytes are unedited -- not that these rules are
911
+ # the right ones to judge them by.
912
+ schema_reason = check_schema_version(proof)
913
+ result["schema_version"] = proof.get("schema_version")
914
+ result["schema_supported"] = schema_reason is None
915
+ if schema_reason:
916
+ result["reasons"].append(schema_reason)
917
+
866
918
  # ----- overall verdict -------------------------------------------------
867
919
  result["ok"] = bool(
868
920
  integrity["ok"]
869
921
  and result["diff_drift"] is False
870
922
  and (not recorded_tree or result["tree_drift"] is False)
923
+ and schema_reason is None
871
924
  )
872
925
  if result["ok"]:
873
926
  result["reason"] = ""