loki-mode 9.19.1 → 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 (83) 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 +376 -10
  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 +65 -0
  13. package/docs/COMPETITIVE-SCORECARD.md +31 -0
  14. package/docs/PROOF-PASSPORT.md +29 -0
  15. package/docs/SIGNED-RECEIPTS.md +111 -0
  16. package/docs/metrics.md +32 -0
  17. package/events/emit.sh +31 -0
  18. package/loki-ts/dist/loki.js +2 -2
  19. package/mcp/__init__.py +1 -1
  20. package/package.json +1 -1
  21. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  22. package/tools/proof-passport.py +175 -0
  23. package/web-app/dist/assets/{AdminPage-CcCJ0Sjt.js → AdminPage-DNGNuAge.js} +1 -1
  24. package/web-app/dist/assets/{Avatar-DK8kmayw.js → Avatar-CqAveJaT.js} +1 -1
  25. package/web-app/dist/assets/{Badge-4uAWnemi.js → Badge-BtMejA0Z.js} +1 -1
  26. package/web-app/dist/assets/{Button-BBMk33tk.js → Button-CikxmqSs.js} +1 -1
  27. package/web-app/dist/assets/{ComparePage-bt9rwvST.js → ComparePage-BB8DJBWR.js} +1 -1
  28. package/web-app/dist/assets/EvidenceReceiptPanel-6oquKEjd.js +1 -0
  29. package/web-app/dist/assets/{GitHubIssuesPanel-WDbH47UM.js → GitHubIssuesPanel-5XLYWzWH.js} +1 -1
  30. package/web-app/dist/assets/{GitHubPRsPanel-C2CiYtTx.js → GitHubPRsPanel-DUfmFYeu.js} +1 -1
  31. package/web-app/dist/assets/{HomePage-BQk-MUjn.js → HomePage-DZ4MmOdH.js} +5 -5
  32. package/web-app/dist/assets/{LoginPage-DMOZVGGL.js → LoginPage-VMzI6ROD.js} +1 -1
  33. package/web-app/dist/assets/{MagicPage-Bzp2Nt1z.js → MagicPage-CFbX5gWg.js} +1 -1
  34. package/web-app/dist/assets/{MetricsPage-C39JVdsw.js → MetricsPage-DbL6xmRQ.js} +2 -2
  35. package/web-app/dist/assets/{NotFoundPage-6vT_U9UL.js → NotFoundPage-BUrjXPAR.js} +1 -1
  36. package/web-app/dist/assets/{ProjectPage-BfFcZp-E.js → ProjectPage-iP6dJihg.js} +3 -3
  37. package/web-app/dist/assets/{ProjectsPage-CPMBf8Wt.js → ProjectsPage-BibN9bHl.js} +1 -1
  38. package/web-app/dist/assets/{SettingsPage-BnNN6ETl.js → SettingsPage-C0WJp1hW.js} +1 -1
  39. package/web-app/dist/assets/{ShowcasePage-WDrMf-cx.js → ShowcasePage-DUv0Njp8.js} +1 -1
  40. package/web-app/dist/assets/{SystemSettingsPage-DX4jb2e8.js → SystemSettingsPage-BIaCB0Vn.js} +1 -1
  41. package/web-app/dist/assets/{TeamsPage-BCfqcXzu.js → TeamsPage-BE0szv9r.js} +1 -1
  42. package/web-app/dist/assets/{TemplatesPage-CZvmimDj.js → TemplatesPage-BzkA9kat.js} +1 -1
  43. package/web-app/dist/assets/{TerminalOutput-BlRqFwWV.js → TerminalOutput-DBrdT_Fp.js} +1 -1
  44. package/web-app/dist/assets/{activity-CacZsUyr.js → activity-AbfFz38r.js} +1 -1
  45. package/web-app/dist/assets/{bell-DK2qtHnk.js → bell-DF6Z6y5U.js} +1 -1
  46. package/web-app/dist/assets/{bot-CkcUtHad.js → bot-DQ4np_3n.js} +1 -1
  47. package/web-app/dist/assets/{check-CbCPjX3M.js → check-RWuguCwE.js} +1 -1
  48. package/web-app/dist/assets/{chevron-left-5NUKWw3i.js → chevron-left-VpbQPOkC.js} +1 -1
  49. package/web-app/dist/assets/{circle-alert-S7uFoxC2.js → circle-alert-DwT82lJx.js} +1 -1
  50. package/web-app/dist/assets/{clock-CaQRrIrs.js → clock-BBlUa93G.js} +1 -1
  51. package/web-app/dist/assets/{cloud-DBAX6c0r.js → cloud-Dgbz3VCA.js} +1 -1
  52. package/web-app/dist/assets/{code-xml-De5-EXv3.js → code-xml-r6qqION5.js} +1 -1
  53. package/web-app/dist/assets/{copy-CUkT6k1v.js → copy-DMHj52B8.js} +1 -1
  54. package/web-app/dist/assets/{database-BAWf1Gwt.js → database-uJqHSHR1.js} +1 -1
  55. package/web-app/dist/assets/{dollar-sign-Ji8zk86R.js → dollar-sign-BZagljek.js} +1 -1
  56. package/web-app/dist/assets/{file-code-corner-ChtXoBwS.js → file-code-corner-DMopmhu0.js} +1 -1
  57. package/web-app/dist/assets/{file-plus-bFa37P76.js → file-plus-DU_hT_3w.js} +1 -1
  58. package/web-app/dist/assets/{folder-open-DhXpXscO.js → folder-open-BP5TtQAW.js} +1 -1
  59. package/web-app/dist/assets/{git-commit-horizontal-DVPeDQ3j.js → git-commit-horizontal-zFk_IobS.js} +1 -1
  60. package/web-app/dist/assets/{globe-BPZgPeeu.js → globe-mthCRtfp.js} +1 -1
  61. package/web-app/dist/assets/{hammer-jLCaujYH.js → hammer-BWaLBvSA.js} +1 -1
  62. package/web-app/dist/assets/{index-B-0iHBPO.js → index-BXA3UT6P.js} +3 -3
  63. package/web-app/dist/assets/{index-BfZSDej1.css → index-DgpUuqwR.css} +1 -1
  64. package/web-app/dist/assets/{layers-B1vsrsFW.js → layers-Q1LXV5fy.js} +1 -1
  65. package/web-app/dist/assets/{lightbulb-C-uLoq9Y.js → lightbulb-vRRNJ3dB.js} +1 -1
  66. package/web-app/dist/assets/{loader-circle-JTfD-ZuM.js → loader-circle-KgjeHEcV.js} +1 -1
  67. package/web-app/dist/assets/{lock-G9rxD4gZ.js → lock-BerKqno7.js} +1 -1
  68. package/web-app/dist/assets/{mail-BJ0PTN_V.js → mail-XAxTbprX.js} +1 -1
  69. package/web-app/dist/assets/{package-CXClfLOO.js → package-DLPKWAvW.js} +1 -1
  70. package/web-app/dist/assets/{plus-EoL5OCB7.js → plus-NMxxwHMn.js} +1 -1
  71. package/web-app/dist/assets/{refresh-cw-BjREUnVq.js → refresh-cw-Dt42kYtM.js} +1 -1
  72. package/web-app/dist/assets/{rotate-ccw-DahWX07H.js → rotate-ccw-BCR4Tp9u.js} +1 -1
  73. package/web-app/dist/assets/{save-Dek3gCn1.js → save-sR4MJd42.js} +1 -1
  74. package/web-app/dist/assets/{server-D6V1BAia.js → server-SXkchokx.js} +1 -1
  75. package/web-app/dist/assets/{shield-alert-BtTK5Sxb.js → shield-alert-Bvgp6_Dz.js} +1 -1
  76. package/web-app/dist/assets/{trash-2-BT5o_g0r.js → trash-2-D0IGudH7.js} +1 -1
  77. package/web-app/dist/assets/{trending-down-D4Jk7KF3.js → trending-down-BbFFBq-v.js} +1 -1
  78. package/web-app/dist/assets/{trending-up-EQFTzhEo.js → trending-up-DDrJKL5i.js} +1 -1
  79. package/web-app/dist/assets/{upload-JfI5lCSE.js → upload-C54rjN8K.js} +1 -1
  80. package/web-app/dist/assets/{usePolling-BnhPUuGd.js → usePolling-D7fw4YSM.js} +1 -1
  81. package/web-app/dist/assets/{user-DSUiUYtj.js → user-Cg7fbzdh.js} +1 -1
  82. package/web-app/dist/index.html +2 -2
  83. package/web-app/server.py +78 -0
package/autonomy/loki CHANGED
@@ -750,7 +750,11 @@ loki_remote_fetch_receipt() {
750
750
  printf '%s' "$resp" > "$tmp" && mv "$tmp" "$dir/proof.json"
751
751
  echo " Evidence Receipt: $dir/proof.json"
752
752
 
753
- loki_remote_verify_receipt "$dir/proof.json"
753
+ # The URL is passed so the receipt's attestation can be checked against that
754
+ # server's published JWKS. Without it the attestation is simply not checked
755
+ # and the verdict falls back to the gpg path, which is the pre-existing
756
+ # behaviour rather than a downgrade.
757
+ loki_remote_verify_receipt "$dir/proof.json" "$url"
754
758
  }
755
759
 
756
760
  # Verify a fetched receipt locally and print exactly what was and was not
@@ -801,6 +805,121 @@ PY
801
805
  esac
802
806
  }
803
807
 
808
+ # Check a receipt's JWKS attestation against the server's PUBLISHED key set.
809
+ #
810
+ # This is the half gpg cannot do over a network. gpg proves provenance only if
811
+ # you already hold the publisher's key, which a submitter who ran
812
+ # `loki start --remote` never does -- so the honest gpg outcome there is
813
+ # NOT CHECKED, and it stays that way until someone performs an out-of-band key
814
+ # import. Fetching /.well-known/jwks.json over the same TLS the API already
815
+ # uses removes that step.
816
+ #
817
+ # Prints "ok", "bad", or "" (could not check). Fails toward "" so an
818
+ # unreachable JWKS never manufactures a verdict in either direction -- an
819
+ # absent measurement is not evidence, and inventing a PASS here would be worse
820
+ # than inventing a FAIL.
821
+ loki_remote_attestation_status() {
822
+ local pj="$1" url="$2"
823
+ [ -n "$url" ] || { printf ''; return 0; }
824
+ command -v python3 >/dev/null 2>&1 || { printf ''; return 0; }
825
+ local tok
826
+ tok="$(jq -r '.verification.attestation // empty' "$pj" 2>/dev/null)"
827
+ [ -n "$tok" ] || { printf ''; return 0; }
828
+ python3 - "$pj" "$url" "${_LOKI_SCRIPT_DIR}" <<'PY' 2>/dev/null || printf ''
829
+ import json, sys, hashlib, urllib.request
830
+ sys.path.insert(0, sys.argv[3])
831
+ try:
832
+ from receipt_jwt import verify_attestation
833
+ except ImportError:
834
+ sys.exit(1) # -> "" : cannot check, never a verdict
835
+ proof = json.load(open(sys.argv[1]))
836
+ tok = (proof.get("verification") or {}).get("attestation") or ""
837
+ if not tok:
838
+ sys.exit(1)
839
+ try:
840
+ with urllib.request.urlopen(
841
+ sys.argv[2].rstrip("/") + "/.well-known/jwks.json", timeout=10) as r:
842
+ jwks = json.loads(r.read())
843
+ except Exception:
844
+ sys.exit(1) # unreachable JWKS is UNCHECKED, not a failure
845
+ ok, claims = verify_attestation(tok, jwks)
846
+ if not ok:
847
+ print("bad"); sys.exit(0)
848
+ # The signature verifying is not enough on its own: it must attest to THIS
849
+ # receipt. Recompute the digest the same way the receipt records it (with
850
+ # `verification` stripped) and require the signed claim to match, or a valid
851
+ # token lifted from a different run would vouch for these bytes.
852
+ proof.pop("verification", None)
853
+ canon = json.dumps(proof, sort_keys=True, separators=(",", ":")).encode()
854
+ print("ok" if claims.get("receipt_sha256") == hashlib.sha256(canon).hexdigest()
855
+ else "bad")
856
+ PY
857
+ }
858
+
859
+ # Check a receipt's attestation against a key set given as a URL OR a file.
860
+ #
861
+ # The file form is what makes this usable by a third party with no network and
862
+ # no Loki install: hand them proof.json and jwks.json and they can check both
863
+ # integrity and provenance themselves. That is the whole claim -- "a receipt you
864
+ # can check yourself" is false if checking requires our infrastructure.
865
+ #
866
+ # Prints "ok", "bad", "none" (no attestation present), or "" (could not check).
867
+ # "none" and "" are deliberately distinct: one is a fact about the receipt, the
868
+ # other is an absent measurement, and collapsing them would report an unchecked
869
+ # receipt as an unattested one.
870
+ # NOTE: this shares its verification logic with loki_remote_attestation_status
871
+ # above. The two differ only in their key-set source (that one is URL-only, from
872
+ # a live fetch during a --remote run; this one also accepts a FILE for the
873
+ # offline third-party case). If you change the digest-recompute rule, CHANGE
874
+ # BOTH -- they are covered by different tests
875
+ # (test-remote-attestation-verdict.sh and test-proof-verify-jwks.sh), so a
876
+ # one-sided edit passes one suite and silently weakens the other. Mutation
877
+ # testing found exactly this: a mutation applied to the first copy left every
878
+ # assertion of the second suite green.
879
+ loki_proof_attestation_check() {
880
+ local pj="$1" src="$2"
881
+ [ -n "$src" ] || { printf ''; return 0; }
882
+ command -v python3 >/dev/null 2>&1 || { printf ''; return 0; }
883
+ python3 - "$pj" "$src" "${_LOKI_SCRIPT_DIR}" <<'PY' 2>/dev/null || printf ''
884
+ import json, sys, os, hashlib, urllib.request
885
+ sys.path.insert(0, sys.argv[3])
886
+ try:
887
+ from receipt_jwt import verify_attestation
888
+ except ImportError:
889
+ sys.exit(1) # "" : cannot check
890
+ try:
891
+ proof = json.load(open(sys.argv[1]))
892
+ except Exception:
893
+ sys.exit(1)
894
+ tok = (proof.get("verification") or {}).get("attestation") or ""
895
+ if not tok:
896
+ print("none"); sys.exit(0) # a FACT about the receipt, not an error
897
+ src = sys.argv[2]
898
+ try:
899
+ if os.path.isfile(src):
900
+ jwks = json.load(open(src))
901
+ else:
902
+ if not src.startswith(("http://", "https://")):
903
+ sys.exit(1)
904
+ url = src if src.rstrip("/").endswith("jwks.json") else \
905
+ src.rstrip("/") + "/.well-known/jwks.json"
906
+ with urllib.request.urlopen(url, timeout=10) as r:
907
+ jwks = json.loads(r.read())
908
+ except Exception:
909
+ sys.exit(1) # "" : unreachable/unreadable, no verdict
910
+ ok, claims = verify_attestation(tok, jwks)
911
+ if not ok:
912
+ print("bad"); sys.exit(0)
913
+ # A verifying signature is not enough: it must attest to THESE bytes. Recompute
914
+ # the digest the way the receipt records it, so a token lifted from another run
915
+ # cannot vouch for this one.
916
+ proof.pop("verification", None)
917
+ canon = json.dumps(proof, sort_keys=True, separators=(",", ":")).encode()
918
+ print("ok" if claims.get("receipt_sha256") == hashlib.sha256(canon).hexdigest()
919
+ else "bad")
920
+ PY
921
+ }
922
+
804
923
  loki_remote_verify_receipt() {
805
924
  local pj="$1"
806
925
  local verifier="${_LOKI_SCRIPT_DIR}/lib/proof-verify.py"
@@ -828,6 +947,10 @@ loki_remote_verify_receipt() {
828
947
  # a gate that cries wolf every time trains users to ignore the verdict.
829
948
  # Only hash_ok and gpg_ok are load-bearing here; drift is not our verdict.
830
949
  local out hash_ok gpg_ok
950
+ # Second argument is the remote URL when this receipt came from a cluster;
951
+ # empty for a local receipt, which simply means no attestation is checked.
952
+ local _attest_url="${2:-}" _attest=""
953
+ _attest="$(loki_remote_attestation_status "$pj" "$_attest_url")"
831
954
  out="$(python3 "$verifier" "$pj" "${TARGET_DIR:-.}" 2>/dev/null)"
832
955
  hash_ok="$(printf '%s' "$out" | jq -r '.hash_ok // false' 2>/dev/null)"
833
956
  gpg_ok="$(printf '%s' "$out" | jq -r '.gpg_ok | tostring' 2>/dev/null)"
@@ -849,6 +972,18 @@ loki_remote_verify_receipt() {
849
972
  echo " the build ran elsewhere. Not tampering, and not part of the verdict."
850
973
  fi
851
974
 
975
+ # An attestation that does not verify is an ACCUSATION, and it is checked
976
+ # before the gpg branches because it is the stronger statement: the server's
977
+ # own published key says this token does not cover these bytes. Either the
978
+ # receipt was altered after the server signed it, or the token was lifted
979
+ # from a different run. Both mean: do not trust this build.
980
+ if [ "$_attest" = "bad" ]; then
981
+ echo -e " ${RED}TAMPERED: the receipt's attestation does not verify against the server's published keys.${NC}" >&2
982
+ echo " Either these bytes were altered after signing, or the attestation" >&2
983
+ echo " belongs to a different run. Do not trust this build." >&2
984
+ return 1
985
+ fi
986
+
852
987
  # gpg_ok=false covers TWO facts that must never be conflated: the signature
853
988
  # is cryptographically bad over this content (the receipt was ALTERED), and
854
989
  # gpg could not evaluate it at all (we do not hold the public key). Only the
@@ -866,6 +1001,17 @@ loki_remote_verify_receipt() {
866
1001
  _why="$(loki_remote_gpg_status "$pj")"
867
1002
  fi
868
1003
  if [ "$_why" = "nopubkey" ]; then
1004
+ # JWKS exists precisely for this case: the gpg key is absent, but a
1005
+ # remote-issued receipt can still prove provenance against the
1006
+ # server's published key set, with no out-of-band import.
1007
+ if [ "$_attest" = "ok" ]; then
1008
+ echo -e " ${GREEN}VERIFIED${NC}: integrity hash matches and the receipt's attestation is valid."
1009
+ echo " Proven: these bytes are unaltered, and the build server signed them"
1010
+ echo " with a key published at ${_attest_url}/.well-known/jwks.json."
1011
+ echo " The gpg key is not in your keyring; the attestation covers provenance."
1012
+ echo " NOT proven: that the code is correct. A receipt records which checks ran."
1013
+ return 0
1014
+ fi
869
1015
  echo -e " ${YELLOW}NOT CHECKED${NC}: the receipt is signed, but the signing key is not in your keyring." >&2
870
1016
  echo " Nothing is wrong with the contents: the integrity hash matches. The" >&2
871
1017
  echo " signature could not be evaluated, so PROVENANCE is unproven here." >&2
@@ -897,6 +1043,20 @@ loki_remote_verify_receipt() {
897
1043
  return 0
898
1044
  fi
899
1045
 
1046
+ # No gpg signature, but a verified attestation. This is the normal shape for
1047
+ # a cluster build: the receiver signs, the build never holds a gpg key, and
1048
+ # provenance is still proven -- so calling it UNSIGNED would understate what
1049
+ # was actually checked, the mirror of the error we refuse in the other
1050
+ # direction.
1051
+ if [ "$_attest" = "ok" ]; then
1052
+ echo -e " ${GREEN}VERIFIED${NC}: integrity hash matches and the receipt's attestation is valid."
1053
+ echo " Proven: these bytes are unaltered, and they were signed by the build"
1054
+ echo " server whose key is published at ${_attest_url}/.well-known/jwks.json."
1055
+ echo " Verify that server is one you trust."
1056
+ echo " NOT proven: that the code is correct. A receipt records which checks ran."
1057
+ return 0
1058
+ fi
1059
+
900
1060
  echo -e " ${YELLOW}UNSIGNED${NC}: integrity hash matches. This is NOT proof of provenance."
901
1061
  echo " Proven: the receipt is internally consistent with its own hash."
902
1062
  echo " NOT proven: who produced it. An unsigned receipt is forgeable -- whoever"
@@ -1501,7 +1661,7 @@ show_landing() {
1501
1661
  }
1502
1662
 
1503
1663
  # Detect argument type for unified `loki start` (v6.84.0)
1504
- # Returns one of: prd | issue | empty | unknown
1664
+ # Returns one of: prd | issue | repo | empty | unknown
1505
1665
  # Logic:
1506
1666
  # - empty arg -> "empty"
1507
1667
  # - file path ending in .md/.json/.txt AND exists on disk -> "prd"
@@ -1565,7 +1725,10 @@ detect_arg_type() {
1565
1725
  # BUT only if a file with that literal name does NOT exist on disk
1566
1726
  # (ambiguity resolution: files-on-disk win over issue-number heuristic).
1567
1727
  if [[ "$arg" =~ ^#?[0-9]+$ ]]; then
1568
- if [ -e "$arg" ]; then
1728
+ if [ -d "$arg" ]; then
1729
+ # Directory on disk wins -- same rule as the -d branch below.
1730
+ echo "repo"
1731
+ elif [ -e "$arg" ]; then
1569
1732
  # File on disk wins -- treat as PRD
1570
1733
  echo "prd"
1571
1734
  else
@@ -1574,6 +1737,20 @@ detect_arg_type() {
1574
1737
  return 0
1575
1738
  fi
1576
1739
 
1740
+ # An existing DIRECTORY is a repo to improve, not a spec to build from.
1741
+ # Before this, a directory fell into the "prd" branch below and was handed
1742
+ # to run.sh as a PRD path; it has no .md-family extension, so the
1743
+ # not-found guard skipped it and the run silently proceeded as if no spec
1744
+ # had been given. `loki start <dir>` now means "work on that repo".
1745
+ # ponytail: a directory named `spec.md` still returns "prd" -- the
1746
+ # extension branch at the top of this function wins. Deliberate: an
1747
+ # explicit spec extension is the stronger signal. Move this block above
1748
+ # that branch if directories-with-spec-names ever matter.
1749
+ if [ -d "$arg" ]; then
1750
+ echo "repo"
1751
+ return 0
1752
+ fi
1753
+
1577
1754
  # An existing file on disk is unambiguously a PRD, even if its name has
1578
1755
  # spaces -- a real path the user pointed at wins over the brief heuristic.
1579
1756
  if [ -e "$arg" ]; then
@@ -1862,9 +2039,10 @@ cmd_start() {
1862
2039
  --help|-h)
1863
2040
  echo -e "${BOLD}loki start${NC} - Unified entry point for Loki Mode (v6.84.0)"
1864
2041
  echo ""
1865
- echo "Usage: loki start [PRD|ISSUE-REF] [options]"
2042
+ echo "Usage: loki start [REPO|PRD|ISSUE-REF] [options]"
1866
2043
  echo ""
1867
2044
  echo "Modes (auto-detected from input):"
2045
+ echo " REPO mode - existing directory -> one bounded verified improvement"
1868
2046
  echo " PRD mode - path ending in .md/.json/.txt/.yaml -> build from PRD"
1869
2047
  echo " ISSUE mode - GitHub/GitLab/Jira/Azure DevOps URL, owner/repo#N,"
1870
2048
  echo " PROJ-123, #123, or bare number -> generate PRD from issue"
@@ -1873,6 +2051,7 @@ cmd_start() {
1873
2051
  echo " no input - analyze current directory, auto-generate PRD"
1874
2052
  echo ""
1875
2053
  echo "Arguments:"
2054
+ echo " REPO Existing project directory"
1876
2055
  echo " PRD Path to PRD file (.md/.json/.txt/.yaml)"
1877
2056
  echo " ISSUE-REF Issue URL, number, or tracker key (see above)"
1878
2057
  echo ""
@@ -1947,6 +2126,7 @@ cmd_start() {
1947
2126
  echo ""
1948
2127
  echo "Examples:"
1949
2128
  echo " loki start # Interactive, analyze current dir"
2129
+ echo " loki start ./my-project # Improve an existing project"
1950
2130
  echo " loki start \"build a todo app\" # BRIEF mode (zero-config fast run)"
1951
2131
  echo " loki start --brief \"snake\" # BRIEF mode (single-word escape hatch)"
1952
2132
  echo " loki start ./prd.md # PRD mode"
@@ -2557,6 +2737,22 @@ cmd_start() {
2557
2737
  detected_type="empty"
2558
2738
  fi
2559
2739
 
2740
+ # Resolve the directory while the positional path is still relative to the
2741
+ # caller, then enter it before creating any .loki state. Quoted `cd --`
2742
+ # keeps spaces and shell metacharacters inert.
2743
+ if [ "$detected_type" = "repo" ]; then
2744
+ local repo_target
2745
+ repo_target=$(cd -- "$positional_arg" 2>/dev/null && pwd -P) || {
2746
+ echo -e "${RED}Error: cannot enter repository directory: $positional_arg${NC}" >&2
2747
+ return 1
2748
+ }
2749
+ cd -- "$repo_target" || return 1
2750
+ if [ "$LOKI_DIR" = ".loki" ]; then
2751
+ LOKI_DIR="$repo_target/.loki"
2752
+ fi
2753
+ export LOKI_REPO_TARGET="$repo_target"
2754
+ fi
2755
+
2560
2756
  # ISSUE mode: delegate to cmd_run's code path. To avoid duplicating ~450
2561
2757
  # lines of issue-fetching + PR-creation logic, we re-dispatch through
2562
2758
  # cmd_run with reconstructed args. cmd_run then calls cmd_start again
@@ -2638,12 +2834,43 @@ cmd_start() {
2638
2834
  echo ""
2639
2835
  fi
2640
2836
 
2837
+ # REPO mode: `loki start <directory>`. We already cd'd into the target
2838
+ # above, so this is the no-arg codebase path with one difference -- a
2839
+ # synthesized brief that bounds the work to ONE high-value improvement
2840
+ # instead of "build out the whole project".
2841
+ #
2842
+ # Deliberately does NOT call set_ttfv_lightweight_profile: that helper
2843
+ # turns the council and the code-review phase OFF, and the whole point of
2844
+ # this entry point is to hand back VERIFIED evidence. Scope is bounded by
2845
+ # the brief text, not by disabling the gates that check the work. The run
2846
+ # keeps full depth, so it reports LOKI_TTFV=repo -- the same honest
2847
+ # full-depth signal the no-arg path uses.
2848
+ if [ "$detected_type" = "repo" ]; then
2849
+ local version
2850
+ version=$(get_version)
2851
+ mkdir -p "$LOKI_DIR" 2>/dev/null || true
2852
+ find "$LOKI_DIR" -maxdepth 1 -name 'repo-prd-*.md' -mtime +1 -delete 2>/dev/null || true
2853
+ local repo_prd="$LOKI_DIR/repo-prd-$$.md"
2854
+ synthesize_repo_prd "$repo_prd"
2855
+ prd_file="$repo_prd"
2856
+
2857
+ echo -e "${BOLD}Loki Mode v$version - Repo improvement${NC}"
2858
+ echo ""
2859
+ echo -e "${CYAN}Repo:${NC} $LOKI_REPO_TARGET"
2860
+ echo -e "${DIM}Loki will inspect this project, pick one high-value improvement,${NC}"
2861
+ echo -e "${DIM}implement it, test it, and produce verified evidence.${NC}"
2862
+ echo -e "${DIM}Full depth: quality gates and completion council stay ON.${NC}"
2863
+ echo -e "${DIM}Scope it yourself instead with: loki start \"<what you want>\".${NC}"
2864
+ echo ""
2865
+ fi
2866
+
2641
2867
  # R7: existing-repo no-arg path also gets the TTFV "what next" framing at the
2642
2868
  # end of the run. Execution here is UNCHANGED (full-depth no-PRD codebase
2643
2869
  # analysis + generated-PRD-reuse); we only add accurate end-of-run framing.
2644
2870
  # The signal value ("repo") drives the full-depth wording in run.sh so the
2645
2871
  # message does not falsely claim a lightweight pass.
2646
- if [ "$detected_type" = "empty" ] && [ -z "${LOKI_TTFV:-}" ]; then
2872
+ if { [ "$detected_type" = "empty" ] || [ "$detected_type" = "repo" ]; } \
2873
+ && [ -z "${LOKI_TTFV:-}" ]; then
2647
2874
  export LOKI_TTFV=repo
2648
2875
  fi
2649
2876
 
@@ -6307,6 +6534,34 @@ else
6307
6534
  DASHBOARD_DEFAULT_HOST="127.0.0.1"
6308
6535
  fi
6309
6536
 
6537
+ # EXPOSED BIND REQUIRES AUTH (issue #188).
6538
+ #
6539
+ # dashboard/auth.py:788 returns True -- allow -- when neither LOKI_ENTERPRISE_AUTH
6540
+ # nor OIDC is configured. That makes all 166 Depends(require_scope) endpoints
6541
+ # open. On loopback that is defensible: the trust boundary is the machine.
6542
+ # Bound to 0.0.0.0 it is not, and NEITHER the Dockerfile NOR the Helm chart set
6543
+ # any auth env, so an operator following the shipped chart gets an
6544
+ # unauthenticated dashboard on every interface without choosing it.
6545
+ #
6546
+ # Refuses to start rather than auto-minting a token. An auto-token an operator
6547
+ # never sees is worse than a refusal: they believe they are protected, and the
6548
+ # credential is sitting in a container they did not know to guard. A refusal is
6549
+ # loud, happens at deploy time rather than after exposure, and names the fix.
6550
+ #
6551
+ # LOKI_DASHBOARD_ALLOW_INSECURE_BIND=1 is the deliberate override, for a cluster
6552
+ # whose network policy already bounds ingress. Named so it appears in a diff and
6553
+ # a reviewer can ask why it is set.
6554
+ _loki_dashboard_bind_is_safe() {
6555
+ local _host="${1:-}"
6556
+ case "$_host" in
6557
+ 127.0.0.1|localhost|::1|"") return 0 ;;
6558
+ esac
6559
+ [ "${LOKI_DASHBOARD_ALLOW_INSECURE_BIND:-}" = "1" ] && return 0
6560
+ [ "${LOKI_ENTERPRISE_AUTH:-}" = "true" ] || [ "${LOKI_ENTERPRISE_AUTH:-}" = "1" ] && return 0
6561
+ [ -n "${LOKI_OIDC_ISSUER:-}" ] && [ -n "${LOKI_OIDC_CLIENT_ID:-}" ] && return 0
6562
+ return 1
6563
+ }
6564
+
6310
6565
  cmd_dashboard() {
6311
6566
  local subcommand="${1:-}"
6312
6567
 
@@ -6461,6 +6716,23 @@ cmd_dashboard_start() {
6461
6716
  esac
6462
6717
  done
6463
6718
 
6719
+ # Refuse an exposed bind with auth disabled (issue #188). Checked AFTER
6720
+ # argument parsing so an explicit --host is covered, not just the default.
6721
+ if ! _loki_dashboard_bind_is_safe "$host"; then
6722
+ echo -e "${RED}Refusing to start: binding $host with authentication disabled.${NC}" >&2
6723
+ echo "" >&2
6724
+ echo " Every dashboard endpoint would be reachable on all interfaces with no" >&2
6725
+ echo " credential. dashboard/auth.py allows all requests when neither" >&2
6726
+ echo " LOKI_ENTERPRISE_AUTH nor OIDC is configured." >&2
6727
+ echo "" >&2
6728
+ echo " Pick one:" >&2
6729
+ echo " LOKI_ENTERPRISE_AUTH=true enable token auth (loki dashboard token create)" >&2
6730
+ echo " LOKI_OIDC_ISSUER + _CLIENT_ID enable OIDC" >&2
6731
+ echo " --host 127.0.0.1 bind loopback only" >&2
6732
+ echo " LOKI_DASHBOARD_ALLOW_INSECURE_BIND=1 accept the exposure deliberately" >&2
6733
+ exit 2
6734
+ fi
6735
+
6464
6736
  # Validate port is numeric
6465
6737
  if [[ ! "$port" =~ ^[0-9]+$ ]]; then
6466
6738
  echo -e "${RED}Invalid port: $port${NC}"
@@ -14795,6 +15067,35 @@ $brief_text
14795
15067
  BRIEFEOF
14796
15068
  }
14797
15069
 
15070
+ # Build a bounded input for `loki start <repo-directory>`. This intentionally
15071
+ # contains no caller-controlled path: repository identity comes from cwd, while
15072
+ # the task remains stable, reviewable, and safe to render into a prompt.
15073
+ synthesize_repo_prd() {
15074
+ local out_file="$1"
15075
+ mkdir -p "$(dirname "$out_file")" 2>/dev/null || true
15076
+ cat > "$out_file" <<'REPOEOF'
15077
+ # Existing Repository Improvement
15078
+
15079
+ ## Outcome
15080
+ Inspect this repository's instructions, product signals, and current tests.
15081
+ Select one small, high-value user-facing improvement and implement it completely.
15082
+
15083
+ ## Success Criteria
15084
+ - The improvement has a clear user outcome and a bounded diff.
15085
+ - Focused tests or checks prove the changed behavior.
15086
+ - Existing unrelated work is preserved.
15087
+ - Loki's normal outcome evidence identifies what changed and what passed.
15088
+
15089
+ ## Constraints
15090
+ - Make one coherent improvement, not a broad rewrite.
15091
+ - Reuse the repository's established architecture and dependencies.
15092
+ - Keep the change easy to review and roll back.
15093
+ - Do not claim deployment, adoption, or customer value without evidence.
15094
+
15095
+ **Mode:** Existing repository improvement (full verification)
15096
+ REPOEOF
15097
+ }
15098
+
14798
15099
  # Quick mode - lightweight single-task execution
14799
15100
  cmd_quick() {
14800
15101
  # v7.6.3 B-11 fix: --help previously fell through to provider invocation
@@ -34878,6 +35179,13 @@ cmd_proof() {
34878
35179
  echo " show <id> Pretty-print .loki/proofs/<id>/proof.json"
34879
35180
  echo " verify <id> Re-check a receipt against the repo (tamper + drift);"
34880
35181
  echo " exit 0 clean, 1 tamper/drift. Verify it yourself."
35182
+ echo ""
35183
+ echo "Options for 'verify':"
35184
+ echo " --human Render the verdict and every failure reason as prose"
35185
+ echo " --jwks <url|file> Also check the receipt's attestation against a published"
35186
+ echo " key set. Accepts a server URL or a local jwks.json, so a"
35187
+ echo " third party can check WHO produced a receipt with no API"
35188
+ echo " token, no gpg key import, and no network."
34881
35189
  echo " md <id> Paste-able Markdown for a PR comment or Slack
34882
35190
  open <id> Open .loki/proofs/<id>/index.html in a browser"
34883
35191
  echo " share <id> Publish the proof page as a GitHub Gist (opt-in)"
@@ -35093,14 +35401,31 @@ PYEOF
35093
35401
  # stdout verbatim, are unaffected. The flag is STRIPPED from the
35094
35402
  # positional args rather than assumed at a fixed index, so it works
35095
35403
  # before or after the id.
35096
- local _pv_human=0 _pv_args=()
35097
- local _pv_a
35404
+ # --jwks <url-or-file> additionally checks the receipt's
35405
+ # attestation against a published key set. This is the third-party
35406
+ # path: an auditor who was handed a proof.json can check WHO
35407
+ # produced it without running a build, without our API token, and
35408
+ # without importing a gpg key. A FILE is accepted as well as a URL
35409
+ # so an air-gapped reviewer, handed the receipt and the key set on
35410
+ # disk, can still verify -- requiring the network would exclude
35411
+ # exactly the reviewer who most needs this.
35412
+ local _pv_human=0 _pv_jwks="" _pv_args=()
35413
+ local _pv_a _pv_want_jwks=0
35098
35414
  for _pv_a in "$@"; do
35415
+ if [ "$_pv_want_jwks" -eq 1 ]; then
35416
+ _pv_jwks="$_pv_a"; _pv_want_jwks=0; continue
35417
+ fi
35099
35418
  case "$_pv_a" in
35100
35419
  --human) _pv_human=1 ;;
35420
+ --jwks) _pv_want_jwks=1 ;;
35421
+ --jwks=*) _pv_jwks="${_pv_a#--jwks=}" ;;
35101
35422
  *) _pv_args+=("$_pv_a") ;;
35102
35423
  esac
35103
35424
  done
35425
+ if [ "$_pv_want_jwks" -eq 1 ]; then
35426
+ echo -e "${RED}--jwks needs a URL or file path.${NC}" >&2
35427
+ exit 2
35428
+ fi
35104
35429
  set -- "${_pv_args[@]+"${_pv_args[@]}"}"
35105
35430
 
35106
35431
  local id="${1:-}"
@@ -35121,12 +35446,53 @@ PYEOF
35121
35446
  echo -e "${RED}Verifier not found (autonomy/lib/proof-verify.py).${NC}"
35122
35447
  exit 2
35123
35448
  fi
35449
+ # `|| _pv_rc=$?` is load-bearing: this script runs under `set -e`
35450
+ # (loki:22), so a bare call to a verifier that exits 1 on drift --
35451
+ # its NORMAL result -- aborts the whole command before the exit
35452
+ # status can be read. Without the guard the attestation block below
35453
+ # is unreachable on exactly the receipts it exists to check.
35454
+ local _pv_rc=0
35124
35455
  if [ "$_pv_human" -eq 1 ]; then
35125
- python3 "$verifier" --human "$pj" "${TARGET_DIR:-.}"
35456
+ python3 "$verifier" --human "$pj" "${TARGET_DIR:-.}" || _pv_rc=$?
35126
35457
  else
35127
- python3 "$verifier" "$pj" "${TARGET_DIR:-.}"
35458
+ python3 "$verifier" "$pj" "${TARGET_DIR:-.}" || _pv_rc=$?
35459
+ fi
35460
+
35461
+ # The attestation check runs AFTER, and can only make the verdict
35462
+ # worse, never better. A valid attestation does not excuse a failed
35463
+ # integrity hash: the signature proves who produced the bytes, not
35464
+ # that the bytes are internally consistent. Folding the two together
35465
+ # would let a signed-but-tampered receipt exit 0.
35466
+ if [ -n "$_pv_jwks" ]; then
35467
+ local _pv_att
35468
+ _pv_att="$(loki_proof_attestation_check "$pj" "$_pv_jwks")"
35469
+ case "$_pv_att" in
35470
+ ok)
35471
+ echo "attestation: VERIFIED against ${_pv_jwks}" >&2
35472
+ echo " Proven: these bytes were signed by the holder of a key in that set." >&2
35473
+ echo " NOT proven: that the code is correct. A receipt records which checks ran." >&2
35474
+ ;;
35475
+ bad)
35476
+ echo -e "${RED}attestation: FAILED -- the token does not cover these bytes.${NC}" >&2
35477
+ echo " Either the receipt was altered after signing, or the attestation" >&2
35478
+ echo " belongs to a different run. Do not trust this receipt." >&2
35479
+ _pv_rc=1
35480
+ ;;
35481
+ none)
35482
+ echo -e "${YELLOW}attestation: ABSENT -- this receipt carries no attestation.${NC}" >&2
35483
+ echo " Nothing was checked against ${_pv_jwks}. The receipt may still be" >&2
35484
+ echo " gpg-signed; this says only that it has no JWKS attestation." >&2
35485
+ ;;
35486
+ *)
35487
+ # Unreachable key set, unreadable file, missing python
35488
+ # deps. NOT a verdict: an absent measurement must never
35489
+ # read as either a pass or an accusation.
35490
+ echo -e "${YELLOW}attestation: NOT CHECKED -- could not read a key set from ${_pv_jwks}.${NC}" >&2
35491
+ echo " This is not a failure of the receipt. Nothing was proven either way." >&2
35492
+ ;;
35493
+ esac
35128
35494
  fi
35129
- exit $?
35495
+ exit "$_pv_rc"
35130
35496
  ;;
35131
35497
  open)
35132
35498
  local id="${1:-}"