loki-mode 9.12.2 → 9.12.4
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/VERSION +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_runs.py +17 -0
- package/dashboard/server.py +56 -0
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.12.
|
|
1
|
+
9.12.4
|
package/dashboard/__init__.py
CHANGED
package/dashboard/api_runs.py
CHANGED
|
@@ -60,6 +60,7 @@ _EFFICIENCY_DIR = ("metrics", "efficiency")
|
|
|
60
60
|
_COMPLETION = ("state", "completion.json")
|
|
61
61
|
_MANIFEST = "loki-run.json"
|
|
62
62
|
_SESSION = "session.json"
|
|
63
|
+
_ORCHESTRATOR = os.path.join("state", "orchestrator.json")
|
|
63
64
|
|
|
64
65
|
# Declared for the envelope so a caller can see exactly what was read, in the
|
|
65
66
|
# style already used at autonomy/loki:21677 (a real path, not a label).
|
|
@@ -227,6 +228,22 @@ def _current_status(loki_dir: str) -> str:
|
|
|
227
228
|
status = session.get("status")
|
|
228
229
|
if status:
|
|
229
230
|
return str(status)
|
|
231
|
+
# THE CANONICAL PHASE, and the reason this fallback exists. session.json is
|
|
232
|
+
# not written by the current runtime -- a live `loki start` produces
|
|
233
|
+
# .loki/state/orchestrator.json instead, and the CLI reads its
|
|
234
|
+
# `currentPhase` (autonomy/loki:4782). Reading only session.json made this
|
|
235
|
+
# API report "unknown" for every live run while the CLI, on the same
|
|
236
|
+
# workspace at the same instant, correctly reported BUILDING.
|
|
237
|
+
#
|
|
238
|
+
# Measured during a real run: CLI phase=BUILDING, API status=unknown.
|
|
239
|
+
# Two surfaces disagreeing about one run is exactly the divergence the
|
|
240
|
+
# operator API exists to prevent, so it now reads the same file the CLI
|
|
241
|
+
# does.
|
|
242
|
+
orch = _read_json(_p(loki_dir, _ORCHESTRATOR))
|
|
243
|
+
if isinstance(orch, dict):
|
|
244
|
+
phase = orch.get("currentPhase") or orch.get("phase")
|
|
245
|
+
if phase:
|
|
246
|
+
return str(phase).lower()
|
|
230
247
|
return "unknown"
|
|
231
248
|
|
|
232
249
|
|
package/dashboard/server.py
CHANGED
|
@@ -1174,6 +1174,62 @@ except Exception as _gzip_exc: # pragma: no cover - starlette always ships it
|
|
|
1174
1174
|
#
|
|
1175
1175
|
# Only MUTATIONS are gated. Reads stay open so a container health probe, a
|
|
1176
1176
|
# metrics scrape and the SPA itself keep working with no configuration.
|
|
1177
|
+
class WebSocketBoundaryMiddleware:
|
|
1178
|
+
"""The same local-or-authenticated rule, for WebSocket scopes.
|
|
1179
|
+
|
|
1180
|
+
WHY A SEPARATE MIDDLEWARE. @app.middleware("http") only wraps HTTP
|
|
1181
|
+
scopes, so every WebSocket route was outside the boundary. Measured with
|
|
1182
|
+
auth off, from a routable remote address, both accepted the connection:
|
|
1183
|
+
|
|
1184
|
+
/ws CONNECTED
|
|
1185
|
+
/ws/collab CONNECTED (and it is WRITABLE -- collaboration state
|
|
1186
|
+
could be pushed by a network caller)
|
|
1187
|
+
|
|
1188
|
+
The routes are not careless: /ws checks a query-parameter token when
|
|
1189
|
+
enterprise auth is ON, and dashboard/server.py:2732 records that
|
|
1190
|
+
FastAPI's Depends() does not work on websocket routes. The hole is the
|
|
1191
|
+
auth-OFF default, where that check is skipped -- exactly the case the
|
|
1192
|
+
HTTP boundary already covers for requests.
|
|
1193
|
+
|
|
1194
|
+
This is plain ASGI rather than a Starlette BaseHTTPMiddleware because the
|
|
1195
|
+
latter has no websocket hook. Registering it here also covers routes
|
|
1196
|
+
added by OTHER modules (collab registers /ws/collab from its own file),
|
|
1197
|
+
which a per-route decorator would miss.
|
|
1198
|
+
|
|
1199
|
+
The decision is shared with the HTTP path: same trusted-proxy resolution,
|
|
1200
|
+
same loopback rule, same auth-enabled deferral. A refused upgrade is
|
|
1201
|
+
closed with policy code 1008 rather than being silently dropped, so a
|
|
1202
|
+
client can tell refusal from a network fault.
|
|
1203
|
+
"""
|
|
1204
|
+
|
|
1205
|
+
def __init__(self, app):
|
|
1206
|
+
self.app = app
|
|
1207
|
+
|
|
1208
|
+
async def __call__(self, scope, receive, send):
|
|
1209
|
+
if scope.get("type") != "websocket":
|
|
1210
|
+
await self.app(scope, receive, send)
|
|
1211
|
+
return
|
|
1212
|
+
if not (auth.ENTERPRISE_AUTH_ENABLED or auth.OIDC_ENABLED):
|
|
1213
|
+
client = scope.get("client")
|
|
1214
|
+
host = client[0] if client else None
|
|
1215
|
+
if host in _trusted_proxies():
|
|
1216
|
+
for raw_name, raw_value in scope.get("headers", []):
|
|
1217
|
+
if raw_name == b"x-forwarded-for":
|
|
1218
|
+
first = raw_value.decode("latin-1").split(",")[0].strip()
|
|
1219
|
+
if first:
|
|
1220
|
+
host = first
|
|
1221
|
+
break
|
|
1222
|
+
if not _is_local_caller(host):
|
|
1223
|
+
# 1008 = policy violation. Closing with a code beats an
|
|
1224
|
+
# accept-then-drop, which reads to a client as a flaky network.
|
|
1225
|
+
await send({"type": "websocket.close", "code": 1008})
|
|
1226
|
+
return
|
|
1227
|
+
await self.app(scope, receive, send)
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
app.add_middleware(WebSocketBoundaryMiddleware)
|
|
1231
|
+
|
|
1232
|
+
|
|
1177
1233
|
@app.middleware("http")
|
|
1178
1234
|
async def dashboard_control_boundary(request: Request, call_next):
|
|
1179
1235
|
# EVERY mutation, plus reads that expose operational or credential-adjacent
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var u_=Object.create;var{getPrototypeOf:p_,defineProperty:eK,getOwnPropertyNames:d_}=Object;var c_=Object.prototype.hasOwnProperty;function l_(Z){return this[Z]}var i_,a_,s_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?i_??=new WeakMap:a_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?u_(p_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of d_(Z))if(!c_.call(K,$))eK(K,$,{get:l_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var n_=(Z)=>Z;function o_(Z,X){this[Z]=n_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:o_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as r_}from"url";import{existsSync as UQ}from"fs";import{homedir as t_}from"os";function e_(){let Z=RO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(t_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(r_(import.meta.url));i0=e_()});import{readFileSync as Zf}from"fs";import{resolve as Xf,dirname as Qf}from"path";import{fileURLToPath as Yf}from"url";function h3(){if(h5!==null)return h5;let Z="9.12.
|
|
2
|
+
var u_=Object.create;var{getPrototypeOf:p_,defineProperty:eK,getOwnPropertyNames:d_}=Object;var c_=Object.prototype.hasOwnProperty;function l_(Z){return this[Z]}var i_,a_,s_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?i_??=new WeakMap:a_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?u_(p_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of d_(Z))if(!c_.call(K,$))eK(K,$,{get:l_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var n_=(Z)=>Z;function o_(Z,X){this[Z]=n_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:o_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as r_}from"url";import{existsSync as UQ}from"fs";import{homedir as t_}from"os";function e_(){let Z=RO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(t_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(r_(import.meta.url));i0=e_()});import{readFileSync as Zf}from"fs";import{resolve as Xf,dirname as Qf}from"path";import{fileURLToPath as Yf}from"url";function h3(){if(h5!==null)return h5;let Z="9.12.4";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Qf(Yf(import.meta.url)),Q=X$(X);h5=Zf(Xf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var bO={};l0(bO,{runOrThrow:()=>Mf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>wf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>yO});async function NQ(Z,X=yO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let W=$.byteLength-(z-X);J+=Y.decode($.subarray(0,W),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function Mf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Tf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Tf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function wf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var yO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return Cf?"":Z}var Cf,L0,F8,p0,KV0,a0,W8,Q9,v;var S6=p(()=>{Cf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),KV0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),W8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as _f}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(_f(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var ZL={};l0(ZL,{runStatus:()=>$h});import{existsSync as Y9,readFileSync as g3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as tf}from"path";import{homedir as ef}from"os";function sO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function nO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*LQ/X);if(J>LQ)J=LQ;let z=LQ-J,K=F8;if(Y>=80)K=L0;else if(Y>=50)K=p0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=sO(Z),V=sO(X);return` ${W8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function Xh(){if(await X9("jq"))return!0;return process.stdout.write(`${L0}Error: jq is required but not installed.${v}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -1232,4 +1232,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
1232
1232
|
`),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (v_(),h_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
|
|
1233
1233
|
`),process.stderr.write(g_),2}}cO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var pW0=await uW0(Bun.argv.slice(2));process.exit(pW0);
|
|
1234
1234
|
|
|
1235
|
-
//# debugId=
|
|
1235
|
+
//# debugId=C7A17AA3F6A8ADEB64756E2164756E21
|
package/mcp/__init__.py
CHANGED
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": "9.12.
|
|
4
|
+
"version": "9.12.4",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "9.12.
|
|
5
|
+
"version": "9.12.4",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|