Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,24 @@ OLLAMA_CHAT_MODEL=llama3.1:8b
INFER_API_KEY=

# store runtime (canonical contract)
# Internal default for one-node compose is service name `infer`.
# Shared defaults — used as fallback when per-purpose vars are not set.
OPENAI_BASE_URL=http://infer:8081/v1
OPENAI_API_KEY=

# Per-purpose inference config.
# Each purpose (main LLM, embedder, graph LLM) can target a different endpoint.
# Falls back to shared OPENAI_BASE_URL / OPENAI_API_KEY when not set.
# This allows e.g. local inference for the main LLM with cloud for embeddings.

# Main LLM (fact extraction + memory decisions)
MEM0_LLM_MODEL=gpt-4.1-nano
# MEM0_LLM_BASE_URL= # falls back to OPENAI_BASE_URL
# MEM0_LLM_API_KEY= # falls back to OPENAI_API_KEY

# Embedder — CANNOT change model after the collection is created
MEM0_EMBED_MODEL=text-embedding-3-small
# MEM0_EMBED_BASE_URL= # falls back to OPENAI_BASE_URL
# MEM0_EMBED_API_KEY= # falls back to OPENAI_API_KEY

# Qdrant config
# Prefer QDRANT_URL when supported by the service runtime.
Expand All @@ -38,3 +51,6 @@ NEO4J_URL=bolt://neo4j:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=changeme
MEM0_GRAPH_LLM_MODEL=gpt-4.1-mini
# MEM0_GRAPH_LLM_BASE_URL= # falls back to OPENAI_BASE_URL
# MEM0_GRAPH_LLM_API_KEY= # falls back to OPENAI_API_KEY
# MEM0_GRAPH_EXTRACTION_STRATEGY=tool_calling # or "json_prompting" for models without tool support
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ The `.env` file configures image tags, model names, and provider keys.
| `QDRANT_API_KEY` | Qdrant auth key (optional) | store runtime | `` |
| `QDRANT_COLLECTION` | Collection name for memory vectors | `store` service | `foxmemory` |

### Per-purpose inference (optional)

Each inference purpose can target a different endpoint. All fall back to `OPENAI_BASE_URL` / `OPENAI_API_KEY` when not set — existing deployments are unaffected.

| Variable | Purpose | Fallback | Example |
|---|---|---|---|
| `MEM0_LLM_BASE_URL` | Main LLM endpoint | `OPENAI_BASE_URL` | `http://exo:8000/v1` |
| `MEM0_LLM_API_KEY` | Main LLM API key | `OPENAI_API_KEY` | `not-needed` |
| `MEM0_EMBED_BASE_URL` | Embedder endpoint | `OPENAI_BASE_URL` | `https://api.openai.com/v1` |
| `MEM0_EMBED_API_KEY` | Embedder API key | `OPENAI_API_KEY` | `sk-...` |
| `MEM0_GRAPH_LLM_BASE_URL` | Graph LLM endpoint | `OPENAI_BASE_URL` | `https://api.openai.com/v1` |
| `MEM0_GRAPH_LLM_API_KEY` | Graph LLM API key | `OPENAI_API_KEY` | `sk-...` |
| `MEM0_GRAPH_EXTRACTION_STRATEGY` | Graph extraction method | `tool_calling` | `json_prompting` |

`MEM0_GRAPH_EXTRACTION_STRATEGY` controls how entities and relationships are extracted from text. Use `tool_calling` (default) for models that support OpenAI-style function calling. Use `json_prompting` for models that don't (e.g. small local models served via EXO).

Backward-compatible variables still present in some scripts/builds:

- `QDRANT_HOST`
Expand Down
4 changes: 4 additions & 0 deletions compose.external.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ services:
- OPENAI_BASE_URL=${OPENAI_BASE_URL}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- MEM0_LLM_MODEL=${MEM0_LLM_MODEL}
- MEM0_LLM_BASE_URL=${MEM0_LLM_BASE_URL:-}
- MEM0_LLM_API_KEY=${MEM0_LLM_API_KEY:-}
- MEM0_EMBED_MODEL=${MEM0_EMBED_MODEL}
- MEM0_EMBED_BASE_URL=${MEM0_EMBED_BASE_URL:-}
- MEM0_EMBED_API_KEY=${MEM0_EMBED_API_KEY:-}
- QDRANT_HOST=qdrant
- QDRANT_PORT=6333
- QDRANT_COLLECTION=${QDRANT_COLLECTION}
Expand Down
7 changes: 7 additions & 0 deletions compose.graph.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ services:
OPENAI_BASE_URL: ${OPENAI_BASE_URL}
OPENAI_API_KEY: ${OPENAI_API_KEY}
MEM0_LLM_MODEL: ${MEM0_LLM_MODEL:-gpt-4.1-nano}
MEM0_LLM_BASE_URL: ${MEM0_LLM_BASE_URL:-}
MEM0_LLM_API_KEY: ${MEM0_LLM_API_KEY:-}
MEM0_EMBED_MODEL: ${MEM0_EMBED_MODEL:-text-embedding-3-small}
MEM0_EMBED_BASE_URL: ${MEM0_EMBED_BASE_URL:-}
MEM0_EMBED_API_KEY: ${MEM0_EMBED_API_KEY:-}
QDRANT_HOST: qdrant
QDRANT_PORT: "6333"
QDRANT_COLLECTION: ${QDRANT_COLLECTION:-foxmemory}
Expand All @@ -46,6 +50,9 @@ services:
NEO4J_USERNAME: ${NEO4J_USERNAME:-neo4j}
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-}
MEM0_GRAPH_LLM_MODEL: ${MEM0_GRAPH_LLM_MODEL:-gpt-4.1-mini}
MEM0_GRAPH_LLM_BASE_URL: ${MEM0_GRAPH_LLM_BASE_URL:-}
MEM0_GRAPH_LLM_API_KEY: ${MEM0_GRAPH_LLM_API_KEY:-}
MEM0_GRAPH_EXTRACTION_STRATEGY: ${MEM0_GRAPH_EXTRACTION_STRATEGY:-}
volumes:
- foxmemory_analytics_data:/data
depends_on:
Expand Down
7 changes: 7 additions & 0 deletions compose.one.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ services:
- OPENAI_BASE_URL=${OPENAI_BASE_URL}
- OPENAI_API_KEY=${INFER_API_KEY}
- MEM0_LLM_MODEL=${MEM0_LLM_MODEL}
- MEM0_LLM_BASE_URL=${MEM0_LLM_BASE_URL:-}
- MEM0_LLM_API_KEY=${MEM0_LLM_API_KEY:-}
- MEM0_EMBED_MODEL=${MEM0_EMBED_MODEL}
- MEM0_EMBED_BASE_URL=${MEM0_EMBED_BASE_URL:-}
- MEM0_EMBED_API_KEY=${MEM0_EMBED_API_KEY:-}
- QDRANT_HOST=127.0.0.1
- QDRANT_PORT=6333
- QDRANT_COLLECTION=${QDRANT_COLLECTION}
Expand All @@ -45,6 +49,9 @@ services:
- NEO4J_USERNAME=${NEO4J_USERNAME:-neo4j}
- NEO4J_PASSWORD=${NEO4J_PASSWORD:-}
- MEM0_GRAPH_LLM_MODEL=${MEM0_GRAPH_LLM_MODEL:-gpt-4.1-mini}
- MEM0_GRAPH_LLM_BASE_URL=${MEM0_GRAPH_LLM_BASE_URL:-}
- MEM0_GRAPH_LLM_API_KEY=${MEM0_GRAPH_LLM_API_KEY:-}
- MEM0_GRAPH_EXTRACTION_STRATEGY=${MEM0_GRAPH_EXTRACTION_STRATEGY:-}
volumes:
- foxmemory_store_data:/qdrant/storage
depends_on:
Expand Down
7 changes: 7 additions & 0 deletions compose.split.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ services:
- OPENAI_BASE_URL=${OPENAI_BASE_URL}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- MEM0_LLM_MODEL=${MEM0_LLM_MODEL}
- MEM0_LLM_BASE_URL=${MEM0_LLM_BASE_URL:-}
- MEM0_LLM_API_KEY=${MEM0_LLM_API_KEY:-}
- MEM0_EMBED_MODEL=${MEM0_EMBED_MODEL}
- MEM0_EMBED_BASE_URL=${MEM0_EMBED_BASE_URL:-}
- MEM0_EMBED_API_KEY=${MEM0_EMBED_API_KEY:-}
- QDRANT_HOST=127.0.0.1
- QDRANT_PORT=6333
- QDRANT_COLLECTION=${QDRANT_COLLECTION}
Expand All @@ -43,6 +47,9 @@ services:
- NEO4J_USERNAME=${NEO4J_USERNAME:-neo4j}
- NEO4J_PASSWORD=${NEO4J_PASSWORD:-}
- MEM0_GRAPH_LLM_MODEL=${MEM0_GRAPH_LLM_MODEL:-gpt-4.1-mini}
- MEM0_GRAPH_LLM_BASE_URL=${MEM0_GRAPH_LLM_BASE_URL:-}
- MEM0_GRAPH_LLM_API_KEY=${MEM0_GRAPH_LLM_API_KEY:-}
- MEM0_GRAPH_EXTRACTION_STRATEGY=${MEM0_GRAPH_EXTRACTION_STRATEGY:-}
volumes:
- foxmemory_store_data:/qdrant/storage
depends_on:
Expand Down
34 changes: 34 additions & 0 deletions docs/NEO4J_AUTH_RUNBOOK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Neo4j Auth Runbook (Environment-Agnostic)

## Goals
- Keep a single source of truth in `.env`
- Prevent auth drift between compose config and Neo4j persisted auth state
- Provide deterministic preflight and rotation helpers

## Files
- `scripts/lib/common.sh`
- `scripts/neo4j-auth-preflight.sh`
- `scripts/neo4j-auth-rotate.sh`

## Required env contract
In `.env`:
- `NEO4J_PASSWORD=<secret>`
- `NEO4J_AUTH=neo4j/${NEO4J_PASSWORD}`

## Preflight check
```bash
scripts/neo4j-auth-preflight.sh
```
Expected output: `NEO4J_AUTH_OK`

## Rotate password safely
```bash
scripts/neo4j-auth-rotate.sh --new-pass 'your-new-strong-password'
```
Expected output:
- `NEO4J_AUTH_OK`
- `NEO4J_PASSWORD_ROTATED_OK`

## Notes
- These scripts avoid hardcoded host paths by deriving compose/env paths from script location, with overrides via `PROJECT_DIR`, `COMPOSE_FILE`, and `ENV_FILE`.
- If your container names differ, set `NEO4J_CONTAINER` before running.
28 changes: 28 additions & 0 deletions scripts/lib/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail

PROJECT_DIR_DEFAULT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PROJECT_DIR="${PROJECT_DIR:-$PROJECT_DIR_DEFAULT}"
COMPOSE_FILE="${COMPOSE_FILE:-$PROJECT_DIR/compose.graph.yml}"
ENV_FILE="${ENV_FILE:-$PROJECT_DIR/.env}"
SERVICE_NEO4J="${SERVICE_NEO4J:-neo4j}"
SERVICE_STORE="${SERVICE_STORE:-store}"

require_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "missing command: $1" >&2; exit 1; }; }

require_env() {
local k="$1"
if ! grep -q "^${k}=" "$ENV_FILE"; then
echo "missing required env var in $ENV_FILE: $k" >&2
exit 1
fi
}

env_get() {
local k="$1"
grep -E "^${k}=" "$ENV_FILE" | tail -n1 | cut -d= -f2-
}

compose() {
docker compose -f "$COMPOSE_FILE" "$@"
}
17 changes: 17 additions & 0 deletions scripts/neo4j-auth-preflight.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/lib/common.sh"

require_cmd docker
require_env NEO4J_PASSWORD
PASS="$(env_get NEO4J_PASSWORD)"
CONTAINER="${NEO4J_CONTAINER:-foxmemory-neo4j}"

echo "[preflight] checking neo4j auth via cypher-shell..."
if docker exec "$CONTAINER" cypher-shell -u neo4j -p "$PASS" "RETURN 1 as ok;" >/tmp/neo4j-preflight.out 2>&1; then
echo "NEO4J_AUTH_OK"
else
echo "NEO4J_AUTH_FAIL"
tail -n 20 /tmp/neo4j-preflight.out || true
exit 1
fi
75 changes: 75 additions & 0 deletions scripts/neo4j-auth-rotate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/lib/common.sh"

usage() {
cat <<USAGE
Usage: $0 --new-pass '<password>' [--no-restart]

Rotates Neo4j password using compose/.env contract, then verifies login.
USAGE
}

NEW_PASS=""
RESTART=1
while [[ $# -gt 0 ]]; do
case "$1" in
--new-pass) NEW_PASS="$2"; shift 2;;
--no-restart) RESTART=0; shift;;
-h|--help) usage; exit 0;;
*) echo "unknown arg: $1"; usage; exit 1;;
esac
done

[[ -n "$NEW_PASS" ]] || { echo "--new-pass is required"; usage; exit 1; }
require_cmd docker
require_env NEO4J_PASSWORD

# Update .env source-of-truth
if grep -q '^NEO4J_PASSWORD=' "$ENV_FILE"; then
sed -i.bak "s#^NEO4J_PASSWORD=.*#NEO4J_PASSWORD=${NEW_PASS}#" "$ENV_FILE"
Comment on lines +29 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update .env only after password rotation succeeds

This script rewrites NEO4J_PASSWORD in .env before attempting any restart or password change in Neo4j. If a later step fails (container name mismatch, startup timing, cypher error), the file now contains a password that was never applied, causing auth drift and breaking subsequent preflight/restart flows until someone manually repairs .env.

Useful? React with 👍 / 👎.

else
echo "NEO4J_PASSWORD=${NEW_PASS}" >> "$ENV_FILE"
fi
if grep -q '^NEO4J_AUTH=' "$ENV_FILE"; then
sed -i.bak "s#^NEO4J_AUTH=.*#NEO4J_AUTH=neo4j/${NEW_PASS}#" "$ENV_FILE"
else
echo "NEO4J_AUTH=neo4j/${NEW_PASS}" >> "$ENV_FILE"
fi

echo "[rotate] updated $ENV_FILE"

if [[ "$RESTART" -eq 1 ]]; then
compose stop "$SERVICE_STORE" || true

# Temporarily disable auth, set password, restore auth contract
python3 - <<PY
from pathlib import Path
p=Path("$COMPOSE_FILE")
s=p.read_text()
s=s.replace('NEO4J_AUTH: neo4j/${NEO4J_PASSWORD}','NEO4J_AUTH: none')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Disable auth using the compose placeholder syntax

The auth toggle targets NEO4J_AUTH: neo4j/${NEO4J_PASSWORD} here, but the default compose file uses NEO4J_AUTH: ${NEO4J_AUTH:-neo4j/changeme} (compose.graph.yml), so this replacement is a no-op in the shipped topology. That leaves auth enabled and makes the subsequent unauthenticated cypher-shell ALTER USER ... step fail, so neo4j-auth-rotate.sh cannot complete successfully out of the box.

Useful? React with 👍 / 👎.

p.write_text(s)
PY

compose up -d "$SERVICE_NEO4J"
sleep 10

CONTAINER="${NEO4J_CONTAINER:-foxmemory-neo4j}"
docker exec "$CONTAINER" cypher-shell "ALTER USER neo4j SET PASSWORD '$NEW_PASS' CHANGE NOT REQUIRED;" >/tmp/neo4j-rotate.out 2>&1 || {
echo "[rotate] failed to set password"; tail -n 30 /tmp/neo4j-rotate.out; exit 1;
}

python3 - <<PY
from pathlib import Path
p=Path("$COMPOSE_FILE")
s=p.read_text()
s=s.replace('NEO4J_AUTH: none','NEO4J_AUTH: neo4j/${NEO4J_PASSWORD}')
p.write_text(s)
PY

compose up -d "$SERVICE_NEO4J" "$SERVICE_STORE"
sleep 10
fi

"$(dirname "$0")/neo4j-auth-preflight.sh"
echo "NEO4J_PASSWORD_ROTATED_OK"
Loading