loki-mode 7.59.2 → 7.60.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 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.59.2
6
+ # Loki Mode v7.60.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.59.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
409
+ **v7.60.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.59.2
1
+ 7.60.0
@@ -79,6 +79,33 @@ _PATTERNS = [
79
79
  # Bearer tokens: keep the scheme, redact the credential.
80
80
  _BEARER = re.compile(r"(Bearer\s+)[A-Za-z0-9._~+/=-]{20,}")
81
81
 
82
+ # HTTP auth/cookie HEADER lines: redact the credential VALUE for the whole line.
83
+ # This owns the header-line form of Authorization (any scheme: Basic, Bearer,
84
+ # Digest, raw token) plus Cookie / Set-Cookie, which the keyword-based
85
+ # _ENV_ASSIGN rule does not cover (Authorization is excluded there via the
86
+ # AUTH(?!ORIZATION) lookahead, and Cookie/session are not secret keywords).
87
+ #
88
+ # Anchored with (?im):
89
+ # - re.IGNORECASE so "authorization"/"Authorization"/"COOKIE" all match.
90
+ # - re.MULTILINE so an interior header line inside a multi-line blob (crash
91
+ # reports, stack traces, env dumps) matches, not just offset 0. "." does
92
+ # not cross newlines, so the value run stops at end-of-line and adjacent
93
+ # normal lines are left untouched.
94
+ #
95
+ # Group layout:
96
+ # 1 -> line prefix up through the "header:" separator (leading whitespace +
97
+ # header name + colon + following whitespace), preserved verbatim.
98
+ # 2 -> optional auth scheme word (Basic/Bearer/Digest/Negotiate/NTLM) plus
99
+ # its trailing space, preserved so "Authorization: Basic [REDACTED]"
100
+ # keeps the scheme. Empty for Cookie / raw-token forms.
101
+ # Everything after that (the credential) is replaced with [REDACTED].
102
+ _AUTH_HEADER = re.compile(
103
+ r"(?im)"
104
+ r"^([ \t]*(?:authorization|cookie|set-cookie)[ \t]*:[ \t]*)" # 1: header prefix
105
+ r"((?:Basic|Bearer|Digest|Negotiate|NTLM)[ \t]+)?" # 2: optional scheme
106
+ r"\S.*$" # the credential value
107
+ )
108
+
82
109
  # PEM PRIVATE KEY blocks (any -----BEGIN ... PRIVATE KEY----- ... END block).
83
110
  # DOTALL so the body spanning newlines is matched and dropped whole.
84
111
  _PEM = re.compile(
@@ -131,6 +158,14 @@ _ENV_ASSIGN = re.compile(
131
158
  )
132
159
 
133
160
 
161
+ def _auth_header_sub(m):
162
+ """Redact an HTTP auth/cookie header VALUE, keeping the header name and
163
+ (when present) the auth scheme word. Group 1 is the "header:" prefix, group
164
+ 2 is the optional scheme (with trailing space) or None."""
165
+ prefix, scheme = m.group(1), m.group(2)
166
+ return prefix + (scheme or "") + "[REDACTED]"
167
+
168
+
134
169
  def _env_assign_sub(m):
135
170
  """Redact a secret assignment value, preserving key, separator and quotes.
136
171
 
@@ -231,6 +266,12 @@ def _redact_value(s):
231
266
  )
232
267
  total += n
233
268
 
269
+ # HTTP auth/cookie header lines: redact the whole credential value before
270
+ # token-level rules run, so a token inside the header value is not matched
271
+ # (and double-counted) separately. Keeps the header name + scheme word.
272
+ s, n = _AUTH_HEADER.subn(_auth_header_sub, s)
273
+ total += n
274
+
234
275
  # Token patterns (ordered most-specific-first).
235
276
  for pat, repl in _PATTERNS:
236
277
  s, n = pat.subn(repl, s)
package/autonomy/loki CHANGED
@@ -16876,7 +16876,11 @@ PYEOF
16876
16876
  export)
16877
16877
  local output="${2:-learnings-export.json}"
16878
16878
 
16879
- LOKI_LEARNINGS_DIR="$learnings_dir" python3 -c "
16879
+ # BUG-PU-010: pass the output filename via an environment variable
16880
+ # rather than interpolating $output into the python -c source, so a
16881
+ # filename containing quotes/backslashes/newlines cannot break out of
16882
+ # the string literal or inject python.
16883
+ LOKI_LEARNINGS_DIR="$learnings_dir" LOKI_LEARNINGS_OUT="$output" python3 -c "
16880
16884
  import json
16881
16885
  import os
16882
16886
 
@@ -16894,9 +16898,10 @@ for category in ['patterns', 'mistakes', 'successes']:
16894
16898
  result[category].append(e)
16895
16899
  except: pass
16896
16900
 
16897
- with open('$output', 'w') as f:
16901
+ _out = os.environ['LOKI_LEARNINGS_OUT']
16902
+ with open(_out, 'w') as f:
16898
16903
  json.dump(result, f, indent=2)
16899
- print(f'Exported to $output')
16904
+ print(f'Exported to {_out}')
16900
16905
  " 2>/dev/null
16901
16906
  ;;
16902
16907
 
@@ -25244,8 +25249,14 @@ generate_component(
25244
25249
 
25245
25250
  # 4. Register in registry
25246
25251
  log_info "Registering component"
25252
+ # BUG-PU-010: pass free-text description/tags via environment variables and
25253
+ # read them with os.environ in the python body, instead of interpolating raw
25254
+ # user input into the python -c source (a value containing triple-quotes,
25255
+ # backslashes, newlines, or $(...) would crash the script or inject code).
25256
+ # Mirrors the LOKI_MEM_QUERY pattern in the memory-search path.
25257
+ LOKI_MAGIC_DESC="$description" LOKI_MAGIC_TAGS="$tags" \
25247
25258
  PYTHONPATH="$(_magic_pypath)" "$py" -c "
25248
- import sys
25259
+ import os, sys
25249
25260
  try:
25250
25261
  from magic.core.registry import register_component
25251
25262
  except Exception as exc:
@@ -25259,8 +25270,8 @@ register_component(
25259
25270
  react_path='.loki/magic/generated/react/${name}.tsx' if '$target' in ('react','both') else '',
25260
25271
  webcomponent_path='.loki/magic/generated/webcomponent/${name}.js' if '$target' in ('webcomponent','both') else '',
25261
25272
  test_path='.loki/magic/generated/tests/${name}.test.tsx',
25262
- description='''$description'''.strip(),
25263
- tags=[t.strip() for t in '''$tags'''.split(',') if t.strip()],
25273
+ description=os.environ.get('LOKI_MAGIC_DESC', '').strip(),
25274
+ tags=[t.strip() for t in os.environ.get('LOKI_MAGIC_TAGS', '').split(',') if t.strip()],
25264
25275
  placement='${placement}' or None,
25265
25276
  )
25266
25277
  " || {
@@ -25305,13 +25316,22 @@ _magic_update() {
25305
25316
  esac
25306
25317
  done
25307
25318
 
25319
+ # BUG-PU-010: validate --name (defense in depth, mirroring _magic_generate)
25320
+ # so a malicious value cannot reach the python body, AND pass it via an
25321
+ # environment variable instead of interpolating it into the python -c source.
25322
+ if [ -n "$name" ] && ! _magic_valid_name "$name"; then
25323
+ log_error "Invalid component name: '$name' (must start with a letter, contain only letters, digits, _ or -)"
25324
+ return 1
25325
+ fi
25326
+
25308
25327
  _magic_ensure_dirs
25309
25328
  local py
25310
25329
  py=$(_magic_python)
25311
25330
 
25312
25331
  log_info "Updating components from specs (name=${name:-<all>}, force=$force)"
25332
+ LOKI_MAGIC_NAME="$name" \
25313
25333
  PYTHONPATH="$(_magic_pypath)" "$py" -c "
25314
- import sys
25334
+ import os, sys
25315
25335
  try:
25316
25336
  from magic.core.generator import update_components
25317
25337
  except Exception as exc:
@@ -25319,7 +25339,7 @@ except Exception as exc:
25319
25339
  sys.exit(2)
25320
25340
  update_components(
25321
25341
  registry_path='.loki/magic/registry.json',
25322
- name='${name}' or None,
25342
+ name=os.environ.get('LOKI_MAGIC_NAME', '') or None,
25323
25343
  force=$([ "$force" = "true" ] && echo True || echo False),
25324
25344
  )
25325
25345
  " || {
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.59.2"
10
+ __version__ = "7.60.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -251,6 +251,25 @@ def resolve_tenant_context(
251
251
  )
252
252
 
253
253
 
254
+ def _require_global_admin(tenant_ctx: TenantContext) -> None:
255
+ """Gate a tenant-lifecycle operation behind global-admin authority.
256
+
257
+ Creating, updating, or deleting a tenant is a global-admin-only operation:
258
+ it manages the isolation boundaries themselves, so a tenant-scoped caller
259
+ (even one holding the `control` scope, which does NOT imply `admin`) must
260
+ not perform it. A global admin is allowed. When auth is disabled there is
261
+ no caller identity to isolate -- single-user local mode -- so the operation
262
+ is permitted, mirroring TenantContext.enforce so legitimate single-tenant
263
+ and local flows are not broken.
264
+ """
265
+ if tenant_ctx.is_global_admin or not tenant_ctx.auth_enabled:
266
+ return
267
+ raise HTTPException(
268
+ status_code=403,
269
+ detail="Tenant lifecycle operations require global admin",
270
+ )
271
+
272
+
254
273
  async def _enforce_project_tenant(
255
274
  db: AsyncSession, tenant_ctx: TenantContext, project_id: int
256
275
  ) -> None:
@@ -306,8 +325,10 @@ async def create_tenant(
306
325
  db: AsyncSession = Depends(get_db),
307
326
  _auth: None = Depends(auth.require_scope("control")),
308
327
  token_info: Optional[dict] = Depends(auth.get_current_token),
328
+ tenant_ctx: TenantContext = Depends(resolve_tenant_context),
309
329
  ):
310
- """Create a new tenant."""
330
+ """Create a new tenant (global-admin only)."""
331
+ _require_global_admin(tenant_ctx)
311
332
  tenant = await tenants_mod.create_tenant(
312
333
  db, name=body.name, description=body.description, settings=body.settings,
313
334
  )
@@ -366,8 +387,8 @@ async def update_tenant(
366
387
  token_info: Optional[dict] = Depends(auth.get_current_token),
367
388
  tenant_ctx: TenantContext = Depends(resolve_tenant_context),
368
389
  ):
369
- """Update an existing tenant."""
370
- tenant_ctx.enforce(tenant_id)
390
+ """Update an existing tenant (global-admin only)."""
391
+ _require_global_admin(tenant_ctx)
371
392
  tenant = await tenants_mod.update_tenant(
372
393
  db, tenant_id,
373
394
  name=body.name, description=body.description, settings=body.settings,
@@ -392,8 +413,8 @@ async def delete_tenant(
392
413
  token_info: Optional[dict] = Depends(auth.get_current_token),
393
414
  tenant_ctx: TenantContext = Depends(resolve_tenant_context),
394
415
  ):
395
- """Delete a tenant."""
396
- tenant_ctx.enforce(tenant_id)
416
+ """Delete a tenant (global-admin only)."""
417
+ _require_global_admin(tenant_ctx)
397
418
  deleted = await tenants_mod.delete_tenant(db, tenant_id)
398
419
  if not deleted:
399
420
  raise HTTPException(status_code=404, detail="Tenant not found")
@@ -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.59.2
5
+ **Version:** v7.60.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.59.2 start ./my-spec.md
398
+ asklokesh/loki-mode:7.60.0 start ./my-spec.md
399
399
  ```
400
400
 
401
401
  ##### docker compose + .env (no host install)
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.59.2";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=XQ(KQ(import.meta.url)),Z=o$(Q);$$=ZQ(zQ(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var a$=L(()=>{b()});var b1={};h(b1,{runOrThrow:()=>qQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>s$});async function k($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function qQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new s$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=VQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function VQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var s$;var d=L(()=>{s$=class s$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return JQ?"":$}var JQ,T,S,_,wZ,I,R,y,V;var c=L(()=>{JQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),_=a("\x1B[1;33m"),wZ=a("\x1B[0;34m"),I=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),V=a("\x1B[0m")});import{existsSync as wQ}from"fs";async function Q$(){if(G$!==void 0)return G$;let $="/opt/homebrew/bin/python3.12";if(wQ($))return G$=$,$;let Q=await f("python3.12");if(Q)return G$=Q,Q;let Z=await f("python3");return G$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var G$;var q$=L(()=>{d()});var e1={};h(e1,{runStatus:()=>uQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as C,basename as DQ}from"path";import{homedir as CQ}from"os";function n1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*k$/Q);if(X>k$)X=k$;let q=k$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${R}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function hQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${V}
2
+ var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.60.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=XQ(KQ(import.meta.url)),Z=o$(Q);$$=ZQ(zQ(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var a$=L(()=>{b()});var b1={};h(b1,{runOrThrow:()=>qQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>s$});async function k($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function qQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new s$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=VQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function VQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var s$;var d=L(()=>{s$=class s$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return JQ?"":$}var JQ,T,S,_,wZ,I,R,y,V;var c=L(()=>{JQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),_=a("\x1B[1;33m"),wZ=a("\x1B[0;34m"),I=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),V=a("\x1B[0m")});import{existsSync as wQ}from"fs";async function Q$(){if(G$!==void 0)return G$;let $="/opt/homebrew/bin/python3.12";if(wQ($))return G$=$,$;let Q=await f("python3.12");if(Q)return G$=Q,Q;let Z=await f("python3");return G$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var G$;var q$=L(()=>{d()});var e1={};h(e1,{runStatus:()=>uQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as C,basename as DQ}from"path";import{homedir as CQ}from"os";function n1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*k$/Q);if(X>k$)X=k$;let q=k$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${R}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function hQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}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)
@@ -790,4 +790,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
790
790
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
791
791
  `),process.stderr.write(s6),2}}l1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var KZ=await XZ(Bun.argv.slice(2));process.exit(KZ);
792
792
 
793
- //# debugId=6FFE51233FAA69E664756E2164756E21
793
+ //# debugId=B963F5BED7BF3C2664756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.59.2'
60
+ __version__ = '7.60.0'
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": "7.59.2",
4
+ "version": "7.60.0",
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": "7.59.2",
5
+ "version": "7.60.0",
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",
@@ -24,9 +24,38 @@ var engine;
24
24
  try {
25
25
  engine = new PolicyEngine(projectDir);
26
26
  } catch (e) {
27
- // No policies configured - allow by default
28
- process.stdout.write(JSON.stringify({ allowed: true, decision: 'ALLOW', reason: 'No policies configured' }));
29
- process.exit(0);
27
+ // A security control that cannot instantiate must FAIL CLOSED (deny),
28
+ // never allow by default. An unexpected error here means we cannot make
29
+ // a sound policy decision, so we deny rather than silently disable
30
+ // enforcement.
31
+ process.stdout.write(JSON.stringify({
32
+ allowed: false,
33
+ decision: 'DENY',
34
+ reason: 'Policy engine failed to initialize: ' + e.message,
35
+ requiresApproval: false,
36
+ violations: [],
37
+ }));
38
+ process.stderr.write('Policy engine failed to initialize: ' + e.message + '\n');
39
+ process.exit(1);
40
+ }
41
+
42
+ // Fail-closed on a present-but-unparseable policy file. If a policy file
43
+ // exists on disk but could not be loaded (corrupt JSON / bad YAML), the engine
44
+ // records the error and leaves _policies null. Falling through to evaluate()
45
+ // would return the misleading "No policies configured" ALLOW, silently
46
+ // disabling all policy enforcement. A security control that disables itself on
47
+ // malformed config is fail-open; deny instead.
48
+ if (engine.hasLoadErrors()) {
49
+ var loadErrors = engine.getValidationErrors();
50
+ process.stdout.write(JSON.stringify({
51
+ allowed: false,
52
+ decision: 'DENY',
53
+ reason: 'Policy file present but could not be loaded (fail-closed): ' + loadErrors.join('; '),
54
+ requiresApproval: false,
55
+ violations: [],
56
+ }));
57
+ process.stderr.write('Policy file present but could not be loaded; denying (fail-closed): ' + loadErrors.join('; ') + '\n');
58
+ process.exit(1);
30
59
  }
31
60
 
32
61
  var result = engine.evaluate(enforcementPoint, context);
@@ -590,6 +590,26 @@ class PolicyEngine {
590
590
  return this._validationErrors;
591
591
  }
592
592
 
593
+ /**
594
+ * Whether a policy file is present on disk but could not be parsed/loaded
595
+ * into a usable policy object.
596
+ *
597
+ * This is the fail-closed discriminator: it is true only when a policy file
598
+ * exists (_policyPath set) yet parsing failed (_policies === null). It is
599
+ * deliberately NOT keyed off getValidationErrors() length, because a valid
600
+ * policy file can still carry soft warnings (e.g. an unrecognized rule
601
+ * string) while parsing cleanly into a non-null _policies object. Those
602
+ * warnings must not disable enforcement.
603
+ *
604
+ * When no policy file exists at all, _policyPath is null and this returns
605
+ * false, preserving the legitimate "no policies -> allow" behavior.
606
+ *
607
+ * @returns {boolean}
608
+ */
609
+ hasLoadErrors() {
610
+ return this._policyPath !== null && this._policies === null;
611
+ }
612
+
593
613
  /**
594
614
  * Stop watching the policy file.
595
615
  */