loki-mode 7.76.0 → 7.77.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/lib/voter-agents.sh +16 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +119 -13
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +190 -190
- package/mcp/__init__.py +1 -1
- package/memory/consolidation.py +9 -4
- package/memory/engine.py +8 -0
- package/memory/retrieval.py +18 -0
- package/memory/storage.py +21 -5
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +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.77.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
406
406
|
|
|
407
407
|
---
|
|
408
408
|
|
|
409
|
-
**v7.
|
|
409
|
+
**v7.77.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.77.0
|
|
@@ -256,7 +256,22 @@ loki_council_dispatch_agents() {
|
|
|
256
256
|
# default-off export in claude-flags.sh (sourced above) already covers this;
|
|
257
257
|
# the inline prefix is belt-and-suspenders, self-documenting, and a no-op when
|
|
258
258
|
# caveman is absent.
|
|
259
|
-
|
|
259
|
+
#
|
|
260
|
+
# timeout-guard the dispatch (parity with the heuristic council path in
|
|
261
|
+
# completion-council.sh:2074 and :2200, same LOKI_COUNCIL_REVIEW_TIMEOUT:-600
|
|
262
|
+
# knob). The heuristic loop this dispatch replaced wrapped every claude
|
|
263
|
+
# subcall in `timeout`; this helper did not, so a hung `claude --agents`
|
|
264
|
+
# would stall the entire run indefinitely. `timeout` precedes the env-var
|
|
265
|
+
# assignment, so `env` is used to set CAVEMAN_DEFAULT_MODE for the child.
|
|
266
|
+
# On timeout, `timeout` exits 124; that exit is the command-substitution exit
|
|
267
|
+
# (no pipe here), captured into rc via `|| rc=$?`. The existing rc check below
|
|
268
|
+
# then routes any non-zero exit (timeout 124 or any other claude failure) to
|
|
269
|
+
# `return 1` -- the heuristic fallback the caller falls through to
|
|
270
|
+
# (completion-council.sh:2792). Fail-closed: a hung or timed-out council can
|
|
271
|
+
# never become a false COMPLETE; it always degrades to the heuristic path
|
|
272
|
+
# (which has its own timeout + conservative defaults).
|
|
273
|
+
response=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" \
|
|
274
|
+
env CAVEMAN_DEFAULT_MODE=off claude --dangerously-skip-permissions \
|
|
260
275
|
-p "$prompt" \
|
|
261
276
|
--agents "$agents_json" \
|
|
262
277
|
--json-schema "$schema_path" 2>"$stderr_log") || rc=$?
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -863,6 +863,111 @@ app.include_router(api_v2_router)
|
|
|
863
863
|
# in web-app/server.py for `loki web` (port 57375). One source of truth, no
|
|
864
864
|
# duplicated UIs. Import is best-effort: if web-app is missing (e.g. partial
|
|
865
865
|
# install) the dashboard still starts; /lab/* returns 404 with a clear hint.
|
|
866
|
+
class _MountAuthGuard:
|
|
867
|
+
"""ASGI wrapper that enforces the dashboard's scope auth at a mount boundary.
|
|
868
|
+
|
|
869
|
+
Starlette does NOT propagate the parent app's route dependencies to a
|
|
870
|
+
mounted sub-app, so without this wrapper the Purple Lab routes (including
|
|
871
|
+
file write/delete and a process-spawn endpoint in web-app/server.py) are
|
|
872
|
+
reachable UNAUTHENTICATED when enterprise auth is enabled. This wrapper
|
|
873
|
+
runs the same token validation as the dashboard's own require_scope("read")
|
|
874
|
+
dependency before delegating to the sub-app.
|
|
875
|
+
|
|
876
|
+
When enterprise auth (and OIDC) are OFF, get_current_token returns None and
|
|
877
|
+
has_scope is never reached: the request passes through unchanged, so local
|
|
878
|
+
default-mode behavior is identical to an unguarded mount.
|
|
879
|
+
"""
|
|
880
|
+
|
|
881
|
+
def __init__(self, app, required_scope: str = "read") -> None:
|
|
882
|
+
self._app = app
|
|
883
|
+
self._required_scope = required_scope
|
|
884
|
+
|
|
885
|
+
@staticmethod
|
|
886
|
+
def _validate_ws_token(token_str: "str | None") -> "dict | None":
|
|
887
|
+
# Mirror the HTTP get_current_token order: try OIDC first for a non-loki_
|
|
888
|
+
# token (JWTs do not carry the loki_ prefix), then fall back to loki token
|
|
889
|
+
# auth. This keeps WS auth consistent with the HTTP path so an OIDC-only
|
|
890
|
+
# deployment can authenticate WS clients too (not just loki_ tokens).
|
|
891
|
+
if not token_str:
|
|
892
|
+
return None
|
|
893
|
+
if auth.is_oidc_mode() and not token_str.startswith("loki_"):
|
|
894
|
+
oidc_info = auth.validate_oidc_token(token_str)
|
|
895
|
+
if oidc_info:
|
|
896
|
+
return oidc_info
|
|
897
|
+
if auth.is_enterprise_mode():
|
|
898
|
+
return auth.validate_token(token_str)
|
|
899
|
+
return None
|
|
900
|
+
|
|
901
|
+
@staticmethod
|
|
902
|
+
def _ws_token_from_scope(scope) -> "str | None":
|
|
903
|
+
# A browser WebSocket cannot set an Authorization header, so accept the
|
|
904
|
+
# token either from an Authorization: Bearer header (programmatic clients)
|
|
905
|
+
# or from a ?token= / ?access_token= query parameter (browser clients),
|
|
906
|
+
# matching how scoped WS clients pass credentials elsewhere.
|
|
907
|
+
for raw_name, raw_val in scope.get("headers", []) or []:
|
|
908
|
+
if raw_name == b"authorization":
|
|
909
|
+
val = raw_val.decode("latin-1", "ignore")
|
|
910
|
+
if val.lower().startswith("bearer "):
|
|
911
|
+
return val[7:].strip() or None
|
|
912
|
+
return val.strip() or None
|
|
913
|
+
from urllib.parse import parse_qs
|
|
914
|
+
qs = scope.get("query_string", b"") or b""
|
|
915
|
+
params = parse_qs(qs.decode("latin-1", "ignore"))
|
|
916
|
+
for key in ("token", "access_token"):
|
|
917
|
+
if params.get(key):
|
|
918
|
+
return params[key][0] or None
|
|
919
|
+
return None
|
|
920
|
+
|
|
921
|
+
async def __call__(self, scope, receive, send) -> None:
|
|
922
|
+
if scope["type"] == "lifespan":
|
|
923
|
+
# Lifespan has no client request; nothing to authenticate.
|
|
924
|
+
await self._app(scope, receive, send)
|
|
925
|
+
return
|
|
926
|
+
if scope["type"] == "websocket":
|
|
927
|
+
# The Purple Lab sub-app exposes WebSocket endpoints (a PTY terminal
|
|
928
|
+
# and an HMR proxy). Starlette does not run the parent auth on them,
|
|
929
|
+
# so without this branch /lab/ws/* would be reachable unauthenticated
|
|
930
|
+
# when enterprise auth is on (a PTY login shell with no auth). Validate
|
|
931
|
+
# the same scoped token as the HTTP path; on failure close the
|
|
932
|
+
# handshake with policy-violation 1008 BEFORE delegating to the sub-app.
|
|
933
|
+
if not auth.is_enterprise_mode() and not auth.is_oidc_mode():
|
|
934
|
+
await self._app(scope, receive, send)
|
|
935
|
+
return
|
|
936
|
+
token_str = self._ws_token_from_scope(scope)
|
|
937
|
+
token_info = self._validate_ws_token(token_str)
|
|
938
|
+
if token_info is None or not auth.has_scope(token_info, self._required_scope):
|
|
939
|
+
# ASGI websocket reject: accept-then-close is the portable way to
|
|
940
|
+
# surface a policy violation to the client before any sub-app code
|
|
941
|
+
# runs. 1008 = policy violation.
|
|
942
|
+
await send({"type": "websocket.close", "code": 1008})
|
|
943
|
+
return
|
|
944
|
+
await self._app(scope, receive, send)
|
|
945
|
+
return
|
|
946
|
+
from starlette.requests import Request as _StarletteRequest
|
|
947
|
+
request = _StarletteRequest(scope, receive)
|
|
948
|
+
# Reuse the exact dashboard auth path: get_current_token is a no-op
|
|
949
|
+
# (returns None) when auth is disabled, raises 401 on missing/bad
|
|
950
|
+
# credentials when enabled.
|
|
951
|
+
try:
|
|
952
|
+
credentials = await auth.security(request)
|
|
953
|
+
token_info = await auth.get_current_token(request, credentials)
|
|
954
|
+
except HTTPException as exc:
|
|
955
|
+
await JSONResponse(
|
|
956
|
+
{"detail": exc.detail},
|
|
957
|
+
status_code=exc.status_code,
|
|
958
|
+
headers=exc.headers,
|
|
959
|
+
)(scope, receive, send)
|
|
960
|
+
return
|
|
961
|
+
# token_info is None only when auth is disabled -> allow through.
|
|
962
|
+
if token_info is not None and not auth.has_scope(token_info, self._required_scope):
|
|
963
|
+
await JSONResponse(
|
|
964
|
+
{"detail": f"Insufficient permissions. Required scope: {self._required_scope}"},
|
|
965
|
+
status_code=403,
|
|
966
|
+
)(scope, receive, send)
|
|
967
|
+
return
|
|
968
|
+
await self._app(scope, receive, send)
|
|
969
|
+
|
|
970
|
+
|
|
866
971
|
_PURPLE_LAB_MOUNTED = False
|
|
867
972
|
try:
|
|
868
973
|
import sys as _sys
|
|
@@ -871,7 +976,9 @@ try:
|
|
|
871
976
|
if str(_webapp_dir) not in _sys.path:
|
|
872
977
|
_sys.path.insert(0, str(_webapp_dir))
|
|
873
978
|
import server as _purple_lab_server # type: ignore[import-not-found]
|
|
874
|
-
|
|
979
|
+
# Gate the mount so /lab/* requires the same scoped token as the dashboard's
|
|
980
|
+
# own endpoints when enterprise auth is on (no-op when auth is off).
|
|
981
|
+
app.mount("/lab", _MountAuthGuard(_purple_lab_server.app, "read"))
|
|
875
982
|
_PURPLE_LAB_MOUNTED = True
|
|
876
983
|
logger.info("Purple Lab mounted at /lab/ (Phase Merge-4)")
|
|
877
984
|
except Exception as _e: # noqa: BLE001
|
|
@@ -6510,8 +6617,8 @@ async def rollback_checkpoint(checkpoint_id: str):
|
|
|
6510
6617
|
# Agent Management API (v5.25.0)
|
|
6511
6618
|
# =============================================================================
|
|
6512
6619
|
|
|
6513
|
-
@app.get("/api/agents")
|
|
6514
|
-
async def get_agents(
|
|
6620
|
+
@app.get("/api/agents", dependencies=[Depends(auth.require_scope("read"))])
|
|
6621
|
+
async def get_agents():
|
|
6515
6622
|
"""Get all active and recent agents."""
|
|
6516
6623
|
agents_file = _get_loki_dir() / "state" / "agents.json"
|
|
6517
6624
|
agents = []
|
|
@@ -6651,8 +6758,8 @@ async def resume_agent(agent_id: str):
|
|
|
6651
6758
|
return {"success": True, "message": f"Resume signal sent to agent {agent_id}"}
|
|
6652
6759
|
|
|
6653
6760
|
|
|
6654
|
-
@app.get("/api/logs")
|
|
6655
|
-
async def get_logs(lines: int = Query(default=100, ge=1, le=10000)
|
|
6761
|
+
@app.get("/api/logs", dependencies=[Depends(auth.require_scope("read"))])
|
|
6762
|
+
async def get_logs(lines: int = Query(default=100, ge=1, le=10000)):
|
|
6656
6763
|
"""Get recent log entries from session log files (redacted)."""
|
|
6657
6764
|
log_dir = _get_loki_dir() / "logs"
|
|
6658
6765
|
entries = []
|
|
@@ -6775,8 +6882,8 @@ async def get_secrets_status():
|
|
|
6775
6882
|
# =============================================================================
|
|
6776
6883
|
|
|
6777
6884
|
|
|
6778
|
-
@app.get("/api/github/status")
|
|
6779
|
-
async def get_github_status(
|
|
6885
|
+
@app.get("/api/github/status", dependencies=[Depends(auth.require_scope("read"))])
|
|
6886
|
+
async def get_github_status():
|
|
6780
6887
|
"""Get GitHub integration status and configuration."""
|
|
6781
6888
|
loki_dir = _get_loki_dir()
|
|
6782
6889
|
result: dict[str, Any] = {
|
|
@@ -6835,8 +6942,8 @@ async def get_github_status(token: Optional[dict] = Depends(auth.get_current_tok
|
|
|
6835
6942
|
return result
|
|
6836
6943
|
|
|
6837
6944
|
|
|
6838
|
-
@app.get("/api/github/tasks")
|
|
6839
|
-
async def get_github_tasks(
|
|
6945
|
+
@app.get("/api/github/tasks", dependencies=[Depends(auth.require_scope("read"))])
|
|
6946
|
+
async def get_github_tasks():
|
|
6840
6947
|
"""Get all GitHub-sourced tasks and their sync status."""
|
|
6841
6948
|
loki_dir = _get_loki_dir()
|
|
6842
6949
|
tasks: list[dict] = []
|
|
@@ -6875,10 +6982,9 @@ async def get_github_tasks(token: Optional[dict] = Depends(auth.get_current_toke
|
|
|
6875
6982
|
return {"tasks": tasks, "total": len(tasks)}
|
|
6876
6983
|
|
|
6877
6984
|
|
|
6878
|
-
@app.get("/api/github/sync-log")
|
|
6985
|
+
@app.get("/api/github/sync-log", dependencies=[Depends(auth.require_scope("read"))])
|
|
6879
6986
|
async def get_github_sync_log(
|
|
6880
6987
|
limit: int = Query(default=50, ge=1, le=500),
|
|
6881
|
-
token: Optional[dict] = Depends(auth.get_current_token)
|
|
6882
6988
|
):
|
|
6883
6989
|
"""Get the GitHub sync log (status updates sent to issues)."""
|
|
6884
6990
|
loki_dir = _get_loki_dir()
|
|
@@ -7007,8 +7113,8 @@ def _resolve_process_state(pid: Optional[int], last_status: str = "",
|
|
|
7007
7113
|
return result
|
|
7008
7114
|
|
|
7009
7115
|
|
|
7010
|
-
@app.get("/api/health/processes")
|
|
7011
|
-
async def get_process_health(
|
|
7116
|
+
@app.get("/api/health/processes", dependencies=[Depends(auth.require_scope("read"))])
|
|
7117
|
+
async def get_process_health():
|
|
7012
7118
|
"""Get health status of all loki processes (dashboard, session, agents).
|
|
7013
7119
|
|
|
7014
7120
|
Returns honest state labels: RUNNING, STALE, COMPLETED, FAILED, CRASHED, UNKNOWN.
|
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.77.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
395
395
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
396
396
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
397
397
|
-v $(pwd):/workspace -w /workspace \
|
|
398
|
-
asklokesh/loki-mode:7.
|
|
398
|
+
asklokesh/loki-mode:7.77.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|