loki-mode 7.5.8 → 7.5.10

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/README.md CHANGED
@@ -64,7 +64,7 @@ loki quick "build a landing page with a signup form"
64
64
  |--------|---------|-------|
65
65
  | **Bun (recommended)** | `bun install -g loki-mode` | Fastest. v8 will be Bun-only. |
66
66
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` | Auto-installs Bun as a dep |
67
- | **Docker** | `docker pull asklokesh/loki-mode:7.5.7 && docker run --rm asklokesh/loki-mode:7.5.7 start prd.md` | Bun pre-installed in image |
67
+ | **Docker** | `docker pull asklokesh/loki-mode:7.5.9 && docker run --rm asklokesh/loki-mode:7.5.9 start prd.md` | Bun pre-installed in image |
68
68
  | **npm (compat)** | `npm install -g loki-mode` | Works without Bun (bash fallback). Migrate any time with `loki self-update --to bun`. |
69
69
 
70
70
  **Upgrading:**
@@ -124,7 +124,7 @@ The next major release sunsets the Bash runtime entirely. There is no firm calen
124
124
  | Method | Command |
125
125
  |--------|---------|
126
126
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` |
127
- | **Docker** | `docker pull asklokesh/loki-mode:7.5.7` |
127
+ | **Docker** | `docker pull asklokesh/loki-mode:7.5.9` |
128
128
  | **Inside Claude Code** | `claude --dangerously-skip-permissions` then type "Loki Mode" |
129
129
  | **Git clone** | `git clone https://github.com/asklokesh/loki-mode.git` |
130
130
 
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.5.8
6
+ # Loki Mode v7.5.10
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -89,7 +89,7 @@ These rules guide autonomous operation. Test results and code quality always tak
89
89
 
90
90
  ## Model Selection
91
91
 
92
- **Default since v5.3.0 (reaffirmed in v7.5.8):** Haiku disabled for quality. Use `--allow-haiku` or `LOKI_ALLOW_HAIKU=true` to enable.
92
+ **Default since v5.3.0 (reaffirmed in v7.5.10):** Haiku disabled for quality. Use `--allow-haiku` or `LOKI_ALLOW_HAIKU=true` to enable.
93
93
 
94
94
  | Task Type | Tier | Claude (default) | Claude (--allow-haiku) | Codex (GPT-5.3) | Gemini |
95
95
  |-----------|------|------------------|------------------------|------------------|--------|
@@ -330,6 +330,19 @@ See `references/core-workflow.md` for the full RARV-C contract.
330
330
 
331
331
  ---
332
332
 
333
+ ## Concurrency and Security Hardening (v7.5.7 - v7.5.10)
334
+
335
+ Three back-to-back patches closed cross-process and security gaps. No user-facing behavior change on the default flow; verify via the cited paths.
336
+
337
+ - **Cross-process file locks** on append-or-rewrite state, so parallel runs / dashboard / MCP do not corrupt shared files: gate counter (`autonomy/run.sh` gate-counter writes), task queues (`autonomy/run.sh` queue read-modify-write), checkpoint index (`autonomy/run.sh` checkpoint index updates), `events.jsonl` append (event emission paths in `events/emit.sh` and `autonomy/run.sh`), human intervention signal files (`autonomy/run.sh:check_human_intervention()` at line ~8059 / 7897 per state-machine doc).
338
+ - **MCP path validation** -- file/path arguments to `mcp/server.py` tools are normalized and rejected if they escape the project root (path-traversal fix from v7.5.8).
339
+ - **Dashboard auth** now required on `/api/memory/*`, `/api/learning/*`, and `/api/status` in `dashboard/server.py` (previously unauthenticated read paths).
340
+ - **Bash quoting hardening** across `autonomy/run.sh` and `autonomy/loki` -- variable expansions inside command substitution and `[ ]` tests quoted to prevent word-splitting on paths with spaces.
341
+
342
+ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.10] for the per-fix list and reviewer sign-off.
343
+
344
+ ---
345
+
333
346
  ## Implemented Features
334
347
 
335
348
  | Feature | Added | Notes |
@@ -365,4 +378,4 @@ See `references/core-workflow.md` for the full RARV-C contract.
365
378
 
366
379
  ---
367
380
 
368
- **v7.5.8 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
381
+ **v7.5.10 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.5.8
1
+ 7.5.10
package/autonomy/run.sh CHANGED
@@ -477,6 +477,18 @@ parse_yaml_with_yq() {
477
477
  load_config_file
478
478
 
479
479
  # Load JSON settings from loki config set (v6.0.0)
480
+ #
481
+ # SECURITY NOTE (v7.5.10, L12#2 audit): The eval below is intentional and safe.
482
+ # The Python script's output is constrained to a fixed template:
483
+ # [ -z "${VAR:-}" ] && export VAR=<value>
484
+ # where:
485
+ # - VAR is a hardcoded env var name from the `mapping` dict (NOT user-controlled).
486
+ # - <value> is produced by shlex.quote(), which emits POSIX-shell-safe single-
487
+ # quoted strings even for adversarial input (e.g. quotes, semicolons, $()).
488
+ # - Non-string values from settings.json are skipped (isinstance check).
489
+ # Therefore no user-controlled bytes can break out of the quoted value or alter
490
+ # the surrounding shell syntax. Do NOT remove the shlex.quote() call or relax
491
+ # the isinstance(val, str) guard without re-auditing this eval.
480
492
  _load_json_settings() {
481
493
  local settings_file="${TARGET_DIR:-.}/.loki/config/settings.json"
482
494
  [ -f "$settings_file" ] || return 0
@@ -7100,13 +7112,31 @@ rollback_to_checkpoint() {
7100
7112
  done
7101
7113
 
7102
7114
  # Log the rollback (use python3 for safe JSON serialization)
7103
- local timestamp
7115
+ # v7.5.10: route through safe_append_event_jsonl() so parallel-worktree
7116
+ # rollbacks cannot interleave partial JSONL lines (POSIX append is
7117
+ # only atomic for <PIPE_BUF and not all platforms honor it).
7118
+ local timestamp rb_event
7104
7119
  timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
7105
- _RB_CPID="$checkpoint_id" _RB_SHA="$git_sha" _RB_TS="$timestamp" \
7120
+ rb_event=$(_RB_CPID="$checkpoint_id" _RB_SHA="$git_sha" _RB_TS="$timestamp" \
7106
7121
  python3 -c "
7107
7122
  import json,os
7108
7123
  print(json.dumps({'event':'rollback','checkpoint':os.environ['_RB_CPID'],'git_sha':os.environ['_RB_SHA'],'timestamp':os.environ['_RB_TS']}))
7109
- " >> ".loki/events.jsonl" 2>/dev/null || true
7124
+ " 2>/dev/null) || rb_event=""
7125
+ if [ -n "$rb_event" ]; then
7126
+ # Source the emit lib once per call to get safe_append_event_jsonl.
7127
+ # Lib-only mode skips the emit script's normal CLI execution.
7128
+ # shellcheck disable=SC1091
7129
+ if [ -z "${_LOKI_EMIT_LIB_LOADED:-}" ]; then
7130
+ LOKI_EMIT_LIB_ONLY=1 . "$(dirname "${BASH_SOURCE[0]}")/../events/emit.sh" 2>/dev/null \
7131
+ && _LOKI_EMIT_LIB_LOADED=1
7132
+ fi
7133
+ if declare -f safe_append_event_jsonl >/dev/null 2>&1; then
7134
+ safe_append_event_jsonl ".loki/events.jsonl" "$rb_event" 2>/dev/null || true
7135
+ else
7136
+ # Last-resort fallback: bare append (preserves prior behavior).
7137
+ printf '%s\n' "$rb_event" >> ".loki/events.jsonl" 2>/dev/null || true
7138
+ fi
7139
+ fi
7110
7140
 
7111
7141
  log_info "State files restored from checkpoint: ${checkpoint_id}"
7112
7142
 
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.5.8"
10
+ __version__ = "7.5.10"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -740,7 +740,7 @@ async def agent_card() -> dict:
740
740
 
741
741
 
742
742
  # Status endpoint - reads from .loki/ flat files (primary) + DB (fallback)
743
- @app.get("/api/status", response_model=StatusResponse)
743
+ @app.get("/api/status", response_model=StatusResponse, dependencies=[Depends(auth.require_scope("read"))])
744
744
  async def get_status() -> StatusResponse:
745
745
  """Get system status from .loki/ session files."""
746
746
  loki_dir = _get_loki_dir()
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.5.8
5
+ **Version:** v7.5.10
6
6
 
7
7
  ---
8
8
 
package/events/emit.sh CHANGED
@@ -11,6 +11,82 @@
11
11
  #
12
12
  # Environment:
13
13
  # LOKI_DIR - Path to .loki directory (default: .loki)
14
+ #
15
+ # Sourcing:
16
+ # This script can also be sourced (LOKI_EMIT_LIB_ONLY=1) to expose the
17
+ # safe_append_event_jsonl() helper without performing an emit.
18
+
19
+ # safe_append_event_jsonl <events_jsonl_path> <line>
20
+ #
21
+ # Cross-process serialized append to .loki/events.jsonl. POSIX append is
22
+ # atomic only for writes <PIPE_BUF (typically 4KB) and not all filesystems
23
+ # honor it; under parallel-worktree contention bare `>>` can interleave
24
+ # partial JSONL lines. This helper serializes appends across processes.
25
+ #
26
+ # Strategy (v7.5.10):
27
+ # - Prefer flock(1) when available (Linux, util-linux). Uses an
28
+ # exclusive lock on a sentinel FD bound to <events>.lock so the lock
29
+ # is released automatically when the subshell exits.
30
+ # - Fall back to a mkdir() mutex on macOS / BSDs where flock is not
31
+ # installed by default. mkdir is atomic on POSIX -- exactly one
32
+ # concurrent caller wins the create. We retry with backoff up to
33
+ # ~5s, and treat a stale lockdir (>30s old) as abandonable.
34
+ # - The newline is appended by the helper -- callers pass the JSON
35
+ # payload only.
36
+ safe_append_event_jsonl() {
37
+ local events_path="$1"
38
+ local line="$2"
39
+ local lock_target="${events_path}.lock"
40
+ local events_dir
41
+ events_dir="$(dirname "$events_path")"
42
+ mkdir -p "$events_dir" 2>/dev/null || true
43
+
44
+ if command -v flock >/dev/null 2>&1; then
45
+ # flock path: bind FD 9 to the sentinel file (created if absent),
46
+ # take an exclusive lock, append, release on subshell exit.
47
+ (
48
+ flock -x 9
49
+ printf '%s\n' "$line" >> "$events_path"
50
+ ) 9>"$lock_target"
51
+ return $?
52
+ fi
53
+
54
+ # Fallback: mkdir-based mutex. mkdir is atomic on POSIX.
55
+ local lock_dir="${events_path}.lockdir"
56
+ local attempts=0
57
+ local max_attempts=500 # ~5s at 10ms sleep
58
+ while ! mkdir "$lock_dir" 2>/dev/null; do
59
+ attempts=$((attempts + 1))
60
+ if [ "$attempts" -ge "$max_attempts" ]; then
61
+ # Stale lock: if the dir is older than 30s, force-remove it.
62
+ local age
63
+ age=$(( $(date +%s) - $(stat -f%m "$lock_dir" 2>/dev/null \
64
+ || stat -c%Y "$lock_dir" 2>/dev/null \
65
+ || echo 0) ))
66
+ if [ "$age" -gt 30 ]; then
67
+ rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir" 2>/dev/null || true
68
+ attempts=0
69
+ continue
70
+ fi
71
+ # Give up -- best-effort write so observability never blocks.
72
+ printf '%s\n' "$line" >> "$events_path" 2>/dev/null || true
73
+ return 1
74
+ fi
75
+ # Sleep ~10ms (perl avoids `sleep 0.01` portability issues).
76
+ perl -e 'select(undef,undef,undef,0.01)' 2>/dev/null || sleep 1
77
+ done
78
+ # Critical section.
79
+ printf '%s\n' "$line" >> "$events_path"
80
+ local rc=$?
81
+ rmdir "$lock_dir" 2>/dev/null || true
82
+ return $rc
83
+ }
84
+
85
+ # Library-only mode: source this file to get safe_append_event_jsonl
86
+ # without executing the emit logic below.
87
+ if [ "${LOKI_EMIT_LIB_ONLY:-0}" = "1" ]; then
88
+ return 0 2>/dev/null || exit 0
89
+ fi
14
90
 
15
91
  set -euo pipefail
16
92
 
@@ -340,7 +340,7 @@ Start a session with: loki start <prd>`}}let Z=u6(X);return{exitCode:0,stdout:Q?
340
340
  `),process.stderr.write(`Usage: loki doctor [--json]
341
341
  `),1;if(Q){let z=await I0();return process.stdout.write(JSON.stringify(z,null,2)+`
342
342
  `),0}return Q3()}var r6,M1,e6,$3;var L0=w(()=>{p();Q1();n();r6=/(\d+\.\d+(?:\.\d+)*)/;M1={fn:y1};e6=[{name:"Claude Code",dir:".claude/skills/loki-mode"},{name:"Codex CLI",dir:".codex/skills/loki-mode"},{name:"Gemini CLI",dir:".gemini/skills/loki-mode"},{name:"Cline CLI",dir:".cline/skills/loki-mode"},{name:"Aider CLI",dir:".aider/skills/loki-mode"}];$3=[{displayName:"Node.js (>= 18)",jsonName:"Node.js",cmd:"node",required:"required",min:"18.0"},{displayName:"Python 3 (>= 3.8)",jsonName:"Python 3",cmd:"python3",required:"required",min:"3.8"},{displayName:"jq",jsonName:"jq",cmd:"jq",required:"required"},{displayName:"git",jsonName:"git",cmd:"git",required:"required"},{displayName:"curl",jsonName:"curl",cmd:"curl",required:"required"},{displayName:"bash (>= 4.0)",jsonName:"bash",cmd:"bash",required:"recommended",min:"4.0"},{displayName:"Bun (>= 1.3)",jsonName:"Bun",cmd:"bun",required:"recommended",min:"1.3"},{displayName:"Claude CLI",jsonName:"Claude CLI",cmd:"claude",required:"optional"},{displayName:"Codex CLI",jsonName:"Codex CLI",cmd:"codex",required:"optional"},{displayName:"Gemini CLI",jsonName:"Gemini CLI",cmd:"gemini",required:"optional"},{displayName:"Cline CLI",jsonName:"Cline CLI",cmd:"cline",required:"optional"},{displayName:"Aider CLI",jsonName:"Aider CLI",cmd:"aider",required:"optional"}]});import{closeSync as A7,fstatSync as T7,lstatSync as O7,mkdirSync as Z3,openSync as j7,readSync as _7,renameSync as K3,rmSync as I7,statSync as x7,unlinkSync as L7,writeFileSync as V3,writeSync as P7}from"fs";import{dirname as W3}from"path";function K1($,Q){Z3(W3($),{recursive:!0});let z=`${$}.tmp.${process.pid}.${++U3}`;V3(z,`${JSON.stringify(Q,null,2)}
343
- `),K3(z,$)}async function P0($,Q){let z=A1.get($)??Promise.resolve(),X=()=>{},Z=new Promise((V)=>{X=V}),K=z.catch(()=>{}).then(()=>Z);A1.set($,K);try{return await z.catch(()=>{}),await Q()}finally{if(X(),A1.get($)===K)A1.delete($)}}var U3=0,A1;var T1=w(()=>{A1=new Map});import{existsSync as O1,mkdirSync as q3,copyFileSync as H3,readFileSync as G3,readdirSync as J3,statSync as B3,writeFileSync as S7,renameSync as Y3,appendFileSync as N7,rmSync as D7}from"fs";import{join as r,dirname as M3}from"path";function j1($){return r($,"state","checkpoints")}function T3($){let Q=j1($);if(!O1(Q))return[];return J3(Q).filter((z)=>z.startsWith("cp-")).filter((z)=>{try{return B3(r(Q,z)).isDirectory()}catch{return!1}})}function O3($){return[...$].sort((Q,z)=>{let X=R0(Q),Z=R0(z);return X-Z})}function R0($){let Q=$.split("-");if(Q.length<3)return 0;let z=Q[Q.length-1],X=Number.parseInt(z??"0",10);return Number.isFinite(X)?X:0}function v1($){let Q=$??P(),z=O3(T3(Q)),X=[];for(let Z of z){let K=E0(Q,Z);if(K)X.push(K)}return X}function E0($,Q){let z=r(j1($),Q,"metadata.json");if(!O1(z))return null;try{let X=JSON.parse(G3(z,"utf-8"));return j3(X,z)}catch{return null}}function j3($,Q){let z=x3($,Q);return z.ok?z.value:null}function x3($,Q){if($===null||typeof $!=="object")return console.warn(`[checkpoint] invalid metadata at ${Q}: not an object`),{ok:!1,reason:"invalid_type",field:"<root>"};let z=$,X=["id","timestamp","task_id","task_description","git_sha","git_branch","provider","phase"];for(let Z of X){if(!(Z in z))return console.warn(`[checkpoint] invalid metadata at ${Q}: field "${Z}" missing`),{ok:!1,reason:"missing_field",field:Z};if(typeof z[Z]!=="string")return console.warn(`[checkpoint] invalid metadata at ${Q}: field "${Z}" not a string`),{ok:!1,reason:"invalid_type",field:Z}}if(!("iteration"in z))return console.warn(`[checkpoint] invalid metadata at ${Q}: field "iteration" missing`),{ok:!1,reason:"missing_field",field:"iteration"};if(typeof z.iteration!=="number"||!Number.isFinite(z.iteration))return console.warn(`[checkpoint] invalid metadata at ${Q}: field "iteration" not a finite number`),{ok:!1,reason:"invalid_type",field:"iteration"};for(let Z of I3){let K=z[Z];if(_3.test(K))return console.warn(`[checkpoint] invalid metadata at ${Q}: field "${Z}" contains control characters`),{ok:!1,reason:"control_chars",field:Z}}return{ok:!0,value:{id:z.id,timestamp:z.timestamp,iteration:z.iteration,task_id:z.task_id,task_description:z.task_description,git_sha:z.git_sha,git_branch:z.git_branch,provider:z.provider,phase:z.phase}}}function m1($,Q){if(!L3.test($))throw new F0($);let z=Q??P(),X=r(j1(z),$);if(!O1(X))throw new h1($);let Z=E0(z,$);if(!Z)throw new h1($);return Z}function w0($,Q){let z=m1($,Q),X=Q??P(),Z=r(j1(X),$),K=[];for(let V of P3){let U=r(Z,V);if(!O1(U))continue;K.push({from:U,to:r(X,V)})}return{id:$,metadata:z,restore:K}}function S0($){let Q=[],z=0;for(let X of $.restore)try{q3(M3(X.to),{recursive:!0});let Z=`${X.to}.tmp.${process.pid}.${++A3}`;H3(X.from,Z),Y3(Z,X.to),z+=1}catch(Z){Q.push(`${X.from} -> ${X.to}: ${Z.message}`)}return{restored:z,errors:Q}}var h7,A3=0,_3,I3,L3,h1,F0,P3;var N0=w(()=>{m();p();T1();h7=Promise.resolve();_3=/[\x00-\x08\x0a-\x1f]/,I3=["id","task_id","git_sha","git_branch","provider","phase"];L3=/^[a-zA-Z0-9_-]+$/;h1=class h1 extends Error{id;constructor($){super(`Checkpoint not found: ${$}`);this.id=$;this.name="CheckpointNotFoundError"}};F0=class F0 extends Error{id;constructor($){super(`Invalid checkpoint ID: must be alphanumeric, hyphens, underscores only (got: ${$})`);this.id=$;this.name="InvalidCheckpointIdError"}};P3=["state/orchestrator.json","queue/pending.json","queue/completed.json","queue/in-progress.json","queue/current-task.json"]});var k0={};b(k0,{runRollback:()=>R3});async function R3($){let Q=$[0],z=$.slice(1);if(Q===void 0||Q==="help"||Q==="--help"||Q==="-h")return process.stdout.write(D0),Q===void 0?1:0;switch(Q){case"list":{let X=[...v1()].reverse();if(X.length===0)return process.stdout.write(`${R}No checkpoints found.${W}
343
+ `),K3(z,$)}async function P0($,Q){let z=A1.get($)??Promise.resolve(),X=()=>{},Z=new Promise((V)=>{X=V}),K=z.catch(()=>{}).then(()=>Z);A1.set($,K);try{return await z.catch(()=>{}),await Q()}finally{if(X(),A1.get($)===K)A1.delete($)}}var U3=0,A1;var T1=w(()=>{A1=new Map});import{existsSync as O1,mkdirSync as q3,copyFileSync as H3,readFileSync as G3,readdirSync as J3,statSync as B3,writeFileSync as S7,renameSync as Y3,appendFileSync as N7,rmSync as D7}from"fs";import{join as r,dirname as M3}from"path";function j1($){return r($,"state","checkpoints")}function T3($){let Q=j1($);if(!O1(Q))return[];return J3(Q).filter((z)=>z.startsWith("cp-")).filter((z)=>{try{return B3(r(Q,z)).isDirectory()}catch{return!1}})}function O3($){return[...$].sort((Q,z)=>{let X=R0(Q),Z=R0(z);return X-Z})}function R0($){let Q=$.split("-");if(Q.length<3)return 0;let z=Q[Q.length-1],X=Number.parseInt(z??"0",10);return Number.isFinite(X)?X:0}function v1($){let Q=$??P(),z=O3(T3(Q)),X=[];for(let Z of z){let K=E0(Q,Z);if(K)X.push(K)}return X}function E0($,Q){let z=r(j1($),Q,"metadata.json");if(!O1(z))return null;try{let X=JSON.parse(G3(z,"utf-8"));return j3(X,z)}catch{return null}}function j3($,Q){let z=x3($,Q);return z.ok?z.value:null}function x3($,Q){if($===null||typeof $!=="object")return console.warn(`[checkpoint] invalid metadata at ${Q}: not an object`),{ok:!1,reason:"invalid_type",field:"<root>"};let z=$,X=["id","timestamp","task_id","task_description","git_sha","git_branch","provider","phase"];for(let Z of X){if(!(Z in z))return console.warn(`[checkpoint] invalid metadata at ${Q}: field "${Z}" missing`),{ok:!1,reason:"missing_field",field:Z};if(typeof z[Z]!=="string")return console.warn(`[checkpoint] invalid metadata at ${Q}: field "${Z}" not a string`),{ok:!1,reason:"invalid_type",field:Z}}if(!Object.prototype.hasOwnProperty.call(z,"iteration"))return console.warn(`[checkpoint] invalid metadata at ${Q}: field "iteration" missing`),{ok:!1,reason:"missing_field",field:"iteration"};if(typeof z.iteration!=="number"||!Number.isFinite(z.iteration))return console.warn(`[checkpoint] invalid metadata at ${Q}: field "iteration" not a finite number`),{ok:!1,reason:"invalid_type",field:"iteration"};for(let Z of I3){let K=z[Z];if(_3.test(K))return console.warn(`[checkpoint] invalid metadata at ${Q}: field "${Z}" contains control characters`),{ok:!1,reason:"control_chars",field:Z}}return{ok:!0,value:{id:z.id,timestamp:z.timestamp,iteration:z.iteration,task_id:z.task_id,task_description:z.task_description,git_sha:z.git_sha,git_branch:z.git_branch,provider:z.provider,phase:z.phase}}}function m1($,Q){if(!L3.test($))throw new F0($);let z=Q??P(),X=r(j1(z),$);if(!O1(X))throw new h1($);let Z=E0(z,$);if(!Z)throw new h1($);return Z}function w0($,Q){let z=m1($,Q),X=Q??P(),Z=r(j1(X),$),K=[];for(let V of P3){let U=r(Z,V);if(!O1(U))continue;K.push({from:U,to:r(X,V)})}return{id:$,metadata:z,restore:K}}function S0($){let Q=[],z=0;for(let X of $.restore)try{q3(M3(X.to),{recursive:!0});let Z=`${X.to}.tmp.${process.pid}.${++A3}`;H3(X.from,Z),Y3(Z,X.to),z+=1}catch(Z){Q.push(`${X.from} -> ${X.to}: ${Z.message}`)}return{restored:z,errors:Q}}var h7,A3=0,_3,I3,L3,h1,F0,P3;var N0=w(()=>{m();p();T1();h7=Promise.resolve();_3=/[\x00-\x08\x0a-\x1f\x7f-\x9f]/,I3=["id","task_id","git_sha","git_branch","provider","phase"];L3=/^[a-zA-Z0-9_-]+$/;h1=class h1 extends Error{id;constructor($){super(`Checkpoint not found: ${$}`);this.id=$;this.name="CheckpointNotFoundError"}};F0=class F0 extends Error{id;constructor($){super(`Invalid checkpoint ID: must be alphanumeric, hyphens, underscores only (got: ${$})`);this.id=$;this.name="InvalidCheckpointIdError"}};P3=["state/orchestrator.json","queue/pending.json","queue/completed.json","queue/in-progress.json","queue/current-task.json"]});var k0={};b(k0,{runRollback:()=>R3});async function R3($){let Q=$[0],z=$.slice(1);if(Q===void 0||Q==="help"||Q==="--help"||Q==="-h")return process.stdout.write(D0),Q===void 0?1:0;switch(Q){case"list":{let X=[...v1()].reverse();if(X.length===0)return process.stdout.write(`${R}No checkpoints found.${W}
344
344
  `),0;process.stdout.write(`${D}Checkpoints${W} (${X.length}, newest first):
345
345
  `);for(let Z of X)process.stdout.write(` ${A}${Z.id}${W} iter=${Z.iteration} ${Z.git_branch||"(no branch)"}@${(Z.git_sha||"").slice(0,7)} ${Z.timestamp}
346
346
  `);return 0}case"show":{let X=z[0];if(!X)return process.stderr.write(`${x}Missing checkpoint id.${W} Use \`loki rollback list\`.
@@ -423,7 +423,7 @@ Subcommands:
423
423
 
424
424
  This command is invoked by autonomy/run.sh between iterations. Users
425
425
  should not run it directly -- run \`loki start\` instead.
426
- `,T5;var $6=w(()=>{m();T1();T5=c1});m();import{readFileSync as U6}from"fs";import{resolve as q6,dirname as H6}from"path";import{fileURLToPath as G6}from"url";var o=null;function r1(){if(o!==null)return o;let $="7.5.8";if(typeof $==="string"&&$.length>0)return o=$,o;try{let Q=H6(G6(import.meta.url)),z=E1(Q);o=U6(q6(z,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}function s1(){return process.stdout.write(`Loki Mode v${r1()}
426
+ `,T5;var $6=w(()=>{m();T1();T5=c1});m();import{readFileSync as U6}from"fs";import{resolve as q6,dirname as H6}from"path";import{fileURLToPath as G6}from"url";var o=null;function r1(){if(o!==null)return o;let $="7.5.10";if(typeof $==="string"&&$.length>0)return o=$,o;try{let Q=H6(G6(import.meta.url)),z=E1(Q);o=U6(q6(z,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}function s1(){return process.stdout.write(`Loki Mode v${r1()}
427
427
  `),0}p();n();m();import{readFileSync as A6,existsSync as T6}from"fs";import{resolve as O6}from"path";var j6=["claude","codex","gemini","cline","aider"];function i1(){let $=O6(P(),"state","provider");if(!T6($))return"";try{return A6($,"utf-8").trim()}catch{return""}}function _6($,Q){return $||Q||process.env.LOKI_PROVIDER||"claude"}function I6($){let Q=i1(),z=_6($,Q);switch(process.stdout.write(`${D}Current Provider${W}
428
428
  `),process.stdout.write(`
429
429
  `),process.stdout.write(`${A}Provider:${W} ${z}
@@ -506,4 +506,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
506
506
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
507
507
  `),process.stderr.write(z6),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var _5=await j5(Bun.argv.slice(2));process.exit(_5);
508
508
 
509
- //# debugId=6C8E7FC38A38CF9964756E2164756E21
509
+ //# debugId=70EC2B659E43E8E764756E2164756E21
@@ -5,7 +5,8 @@
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "engines": {
8
- "bun": ">=1.3.0"
8
+ "bun": ">=1.3.0",
9
+ "node": ">=20.0.0"
9
10
  },
10
11
  "scripts": {
11
12
  "version": "bun src/cli.ts version",
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.5.8'
60
+ __version__ = '7.5.10'
@@ -295,6 +295,42 @@ class MemoryRetrieval:
295
295
  """Get the current namespace."""
296
296
  return self._namespace
297
297
 
298
+ # Track which legacy entries we've already warned about to avoid log spam.
299
+ _LEGACY_WARN_LIMIT = 5
300
+ _legacy_warned_count: int = 0
301
+
302
+ def _belongs_to_namespace(self, result: Dict[str, Any]) -> bool:
303
+ """
304
+ Check if a memory result belongs to the current namespace.
305
+
306
+ Cross-namespace leak defense (v7.5.10). Storage layer should isolate
307
+ files by directory, but this filter provides defense-in-depth and
308
+ handles cases where vector indices span namespaces.
309
+
310
+ Behavior:
311
+ - If self._namespace is None, accept all (backward compat for unscoped retrieval).
312
+ - If result has "_namespace" matching, accept.
313
+ - If result lacks "_namespace" (legacy entry written before stamping),
314
+ log a deprecation warning (rate-limited) and ACCEPT for backward compat.
315
+ - Otherwise, reject.
316
+ """
317
+ if self._namespace is None:
318
+ return True
319
+ result_ns = result.get("_namespace")
320
+ if result_ns is None:
321
+ # Legacy entry without namespace stamp; accept for backward compat
322
+ # but warn so operators can re-save to add stamps.
323
+ if MemoryRetrieval._legacy_warned_count < MemoryRetrieval._LEGACY_WARN_LIMIT:
324
+ logger.warning(
325
+ "Memory entry id=%s lacks '_namespace' stamp (legacy "
326
+ "entry). Including in results for backward compatibility. "
327
+ "Re-save this entry to enable namespace isolation.",
328
+ result.get("id", "<unknown>"),
329
+ )
330
+ MemoryRetrieval._legacy_warned_count += 1
331
+ return True
332
+ return result_ns == self._namespace
333
+
298
334
  def with_namespace(self, namespace: str) -> "MemoryRetrieval":
299
335
  """
300
336
  Create a new MemoryRetrieval instance with a different namespace.
@@ -761,7 +797,10 @@ class MemoryRetrieval:
761
797
  item["id"] = item_id
762
798
  item["_score"] = float(score)
763
799
  item["_source"] = collection
764
- items.append(item)
800
+ # Cross-namespace leak defense (v7.5.10): vector indices may be
801
+ # shared across namespaces, so filter here too.
802
+ if self._belongs_to_namespace(item):
803
+ items.append(item)
765
804
 
766
805
  return items
767
806
 
@@ -810,7 +849,8 @@ class MemoryRetrieval:
810
849
  )
811
850
  if data:
812
851
  data["_source"] = "episodic"
813
- results.append(data)
852
+ if self._belongs_to_namespace(data):
853
+ results.append(data)
814
854
 
815
855
  # Filter semantic patterns by last_used
816
856
  patterns_data = self.storage.read_json("semantic/patterns.json") or {}
@@ -831,7 +871,8 @@ class MemoryRetrieval:
831
871
 
832
872
  if since <= last_used_dt <= until:
833
873
  pattern["_source"] = "semantic"
834
- results.append(pattern)
874
+ if self._belongs_to_namespace(pattern):
875
+ results.append(pattern)
835
876
  except (ValueError, TypeError):
836
877
  continue
837
878
 
@@ -1428,7 +1469,8 @@ class MemoryRetrieval:
1428
1469
  data_copy = dict(data)
1429
1470
  data_copy["_score"] = score
1430
1471
  data_copy["_source"] = "episodic"
1431
- results.append(data_copy)
1472
+ if self._belongs_to_namespace(data_copy):
1473
+ results.append(data_copy)
1432
1474
 
1433
1475
  return results
1434
1476
 
@@ -1457,7 +1499,8 @@ class MemoryRetrieval:
1457
1499
  pattern_copy = dict(pattern)
1458
1500
  pattern_copy["_score"] = score
1459
1501
  pattern_copy["_source"] = "semantic"
1460
- results.append(pattern_copy)
1502
+ if self._belongs_to_namespace(pattern_copy):
1503
+ results.append(pattern_copy)
1461
1504
 
1462
1505
  return results
1463
1506
 
@@ -1486,7 +1529,8 @@ class MemoryRetrieval:
1486
1529
  data_copy = dict(data)
1487
1530
  data_copy["_score"] = score
1488
1531
  data_copy["_source"] = "skills"
1489
- results.append(data_copy)
1532
+ if self._belongs_to_namespace(data_copy):
1533
+ results.append(data_copy)
1490
1534
 
1491
1535
  return results
1492
1536
 
@@ -1511,7 +1555,8 @@ class MemoryRetrieval:
1511
1555
  anti_copy = dict(anti)
1512
1556
  anti_copy["_score"] = score
1513
1557
  anti_copy["_source"] = "anti_patterns"
1514
- results.append(anti_copy)
1558
+ if self._belongs_to_namespace(anti_copy):
1559
+ results.append(anti_copy)
1515
1560
 
1516
1561
  return results
1517
1562
 
package/memory/storage.py CHANGED
@@ -391,6 +391,11 @@ class MemoryStorage:
391
391
  episode_id = episode_data.get("id") or self._generate_id("episode")
392
392
  episode_data["id"] = episode_id
393
393
 
394
+ # Stamp namespace so retrieval can verify isolation (cross-namespace
395
+ # leak defense, v7.5.10). Defaults to DEFAULT_NAMESPACE for unscoped
396
+ # storage instances.
397
+ episode_data["_namespace"] = self._namespace or DEFAULT_NAMESPACE
398
+
394
399
  # Determine storage path based on date
395
400
  timestamp = episode_data.get("timestamp", datetime.now(timezone.utc).isoformat())
396
401
  if isinstance(timestamp, str):
@@ -557,6 +562,8 @@ class MemoryStorage:
557
562
  "created_at",
558
563
  datetime.now(timezone.utc).isoformat()
559
564
  )
565
+ # Stamp namespace for cross-namespace leak defense (v7.5.10).
566
+ pattern_data["_namespace"] = self._namespace or DEFAULT_NAMESPACE
560
567
 
561
568
  patterns_path = self.base_path / "semantic" / "patterns.json"
562
569
 
@@ -750,6 +757,8 @@ class MemoryStorage:
750
757
  "created_at",
751
758
  datetime.now(timezone.utc).isoformat()
752
759
  )
760
+ # Stamp namespace for cross-namespace leak defense (v7.5.10).
761
+ skill_data["_namespace"] = self._namespace or DEFAULT_NAMESPACE
753
762
 
754
763
  # Use skill name for filename if available, otherwise use ID
755
764
  skill_name = skill_data.get("name", skill_id)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "7.5.8",
3
+ "version": "7.5.10",
4
4
  "description": "Loki Mode by Autonomi - Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "agent",
@@ -102,7 +102,7 @@
102
102
  ],
103
103
  "scripts": {
104
104
  "prepack": "find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null; find . -name '*.pyc' -delete 2>/dev/null; if command -v bun >/dev/null 2>&1; then (cd loki-ts && bun install --production && bun run build) || echo 'WARN: loki-ts build failed, using existing dist if present'; else echo 'WARN: bun not on PATH, skipping loki-ts build (using committed dist if present)'; fi; true",
105
- "prepublishOnly": "cd dashboard-ui && npm ci && npm run build:all",
105
+ "prepublishOnly": "cd dashboard-ui && npm ci && npm run build:all && test -f ../dashboard/static/index.html",
106
106
  "test": "bash -n autonomy/run.sh && bash -n autonomy/loki && bash -n autonomy/completion-council.sh && bash -n autonomy/app-runner.sh && bash -n autonomy/prd-checklist.sh && bash -n autonomy/playwright-verify.sh && node --test tests/protocols/*.test.js && node --test tests/protocols/a2a/*.test.js && node --test tests/observability/*.test.js && node --test tests/policies/*.test.js && node --test tests/audit/*.test.js && node --test tests/integrations/*.test.js && node --test tests/integrations/jira/*.test.js && node --test tests/integrations/github/*.test.js && node --test tests/integrations/slack/*.test.js && bash tests/managed_memory/test_flag_matrix.sh && bash tests/managed_memory/test_sdk_isolation.sh && bash tests/managed_memory/test_kill_switch.sh && python3 -m unittest tests.managed_memory.test_shadow_write_mock tests.managed_memory.test_retrieve_mock && echo 'All checks passed'",
107
107
  "test:visual": "node --experimental-vm-modules node_modules/jest/bin/jest.js dashboard-ui/tests/visual-regression.test.js",
108
108
  "test:parity": "node --experimental-vm-modules dashboard-ui/scripts/check-parity.js",
@@ -111,7 +111,7 @@
111
111
  "test:integration": "bash tests/integration/run_integration_suite.sh"
112
112
  },
113
113
  "engines": {
114
- "node": ">=18.0.0"
114
+ "node": ">=20.0.0"
115
115
  },
116
116
  "os": [
117
117
  "darwin",
@@ -124,7 +124,7 @@
124
124
  "@opentelemetry/exporter-trace-otlp-http": "^0.57.0"
125
125
  },
126
126
  "overrides": {
127
- "protobufjs": ">=7.5.8"
127
+ "protobufjs": ">=7.5.6"
128
128
  },
129
129
  "devDependencies": {
130
130
  "@types/node": "^25.2.0",
@@ -48,7 +48,7 @@
48
48
 
49
49
  ### quality-gates.md
50
50
  **When:** Code review, pre-commit checks, quality assurance
51
- - 10-gate quality system (Gate 10: backward compatibility for healing)
51
+ - 11-gate quality system (Gate 10: backward compatibility for healing; Gate 11: documentation coverage, v6.75.0)
52
52
  - Blind review + anti-sycophancy
53
53
  - Velocity-quality feedback loop (arXiv research)
54
54
  - Mandatory quality checks per task
package/skills/healing.md CHANGED
@@ -505,3 +505,15 @@ loki heal --report
505
505
  # Strict mode: block any behavioral change without approval
506
506
  LOKI_HEAL_STRICT=true loki heal ./legacy-app
507
507
  ```
508
+
509
+ ## Checkpoint metadata hardening (v7.5.8)
510
+
511
+ As defense-in-depth for the healing checkpoint store, the checkpoint
512
+ writer now rejects ASCII control characters (U+0000-U+001F except TAB,
513
+ LF, CR, plus U+007F) anywhere in checkpoint metadata keys or values
514
+ before persisting `.loki/healing/checkpoints/<phase>.json`. This blocks
515
+ log-injection and JSON-poisoning vectors where adapter output, friction
516
+ notes, or institutional-knowledge excerpts could smuggle terminal
517
+ escapes or NUL bytes into the resume path. Rejected writes raise a
518
+ typed error and never partially overwrite the prior checkpoint, so
519
+ `loki heal --resume` always restarts from a clean, validated state.
@@ -160,6 +160,16 @@ are silently dropped at load time. The override council uses a stub
160
160
  judge in v7.5.x that approves any of those six trusted proofTypes;
161
161
  real provider-backed judges land in Phase 2 of Part B.
162
162
 
163
+ **Cross-process gate counter (v7.5.5+)**: the per-iteration gate counter
164
+ at `.loki/state/gate-counter-<iter>.json` is now incremented under a
165
+ cross-process file lock via `withFileLockSync` in
166
+ `loki-ts/src/util/atomic.ts`. Concurrent gate runs (parallel worktrees,
167
+ overlapping `runQualityGates` invocations) no longer race the
168
+ read-modify-write, so override-council quotas and per-finding counters
169
+ remain consistent across processes. The lock file lives at
170
+ `.loki/state/gate-counter-<iter>.json.lock` and is released even on
171
+ crash via the primitive's `finally` cleanup.
172
+
163
173
  ---
164
174
 
165
175
  ## Guardrails Execution Modes
@@ -25,3 +25,6 @@ passlib[bcrypt]>=1.7.4
25
25
  # Encryption (optional) -- Fernet encryption for user secrets.
26
26
  # Needed only for the /api/secrets endpoints in cloud mode.
27
27
  cryptography>=41.0.0
28
+
29
+ # YAML parsing -- used by server.py for config loading
30
+ pyyaml>=6.0