loki-mode 9.12.2 → 9.12.3

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 CHANGED
@@ -1 +1 @@
1
- 9.12.2
1
+ 9.12.3
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.12.2"
10
+ __version__ = "9.12.3"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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
@@ -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";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}
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.3";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=23B556726095B27C64756E2164756E21
1235
+ //# debugId=B21780545C2EE56864756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '9.12.2'
78
+ __version__ = '9.12.3'
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.2",
4
+ "version": "9.12.3",
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.2",
5
+ "version": "9.12.3",
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",