Skip to content
Merged
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
50 changes: 50 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Source control and local instructions
.git
.github
AGENTS.md
.codewhale/
.vscode/
.idea/
.DS_Store

# Python environments, caches, and test output
venv/
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/

# Local runtime stores and derived indexes
chroma_memory/
chroma_index/
graphify-out/
*.sqlite
*.sqlite3
*.sqlite-*
*.sqlite3-*
*.db
*.db-*
*.log
.kyrozen/
.kyrozen_config.json

# Build and packaging output
build/
dist/
*.egg-info/

# Credentials and local configuration
.env
.env.*
*.pem
*.key
*.crt
*.cer
*.p12
*.pfx
credentials*
secrets*
11 changes: 9 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,12 @@ jobs:
run: |
python -c "from providers import *; print('All providers imported OK')"

- name: Docker build check
run: docker build -t openkyrozen-test .
docker-persistence:
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v4

- name: Build and replace-container persistence smoke test
run: make docker-smoke
13 changes: 10 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpenKyrozen Docker Image
# Build: docker build -t openkyrozen .
# Run: docker run -p 8000:8000 -e DEEPSEEK_API_KEY=sk-... openkyrozen
# Run: docker run -p 8000:8000 -v kyrozen-data:/data openkyrozen

FROM python:3.12-slim

Expand All @@ -19,13 +19,20 @@ RUN pip install --no-cache-dir fastapi uvicorn
# Copy application code
COPY . .

# Run the service without root privileges. The application only needs to read
# its code and write user data under the mounted working directory/home.
# Keep the SQLite source of truth and its derived index under one explicit,
# non-root-writable data directory. A named Docker volume mounted at /data is
# therefore sufficient to survive container replacement.
ENV KYROZEN_DB_PATH=/data/openkyrozen.sqlite3 \
KYROZEN_VECTOR_PATH=/data/chroma_index

RUN useradd --create-home --uid 10001 --shell /usr/sbin/nologin kyrozen \
&& install -d -o kyrozen -g kyrozen /data \
&& chown -R kyrozen:kyrozen /app

USER kyrozen

VOLUME ["/data"]

# Expose web server port
EXPOSE 8000

Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: install run clean lint test check benchmark git-status git-diff git-log web
.PHONY: install run clean lint test check benchmark docker-smoke git-status git-diff git-log web

# Prefer the known-stable Python 3.12, but use the active supported Python
# 3.13 on clean runners that do not provide 3.12. Python 3.14 remains
Expand Down Expand Up @@ -68,6 +68,11 @@ benchmark:
--evolved-runner "$(VENV_PYTHON) benchmarks/evolved_runner.py" \
--timeout "$${BENCHMARK_TIMEOUT:-60}"

docker-smoke:
@command -v docker >/dev/null 2>&1 || { echo "Error: Docker is required for the container smoke test."; exit 1; }
docker build -t openkyrozen-smoke .
./scripts/docker_persistence_smoke.sh openkyrozen-smoke

# Quick verification
check:
@echo "Checking Python syntax..."
Expand Down
12 changes: 10 additions & 2 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,11 @@ python server.py --port 8000

# または Docker 経由:
docker build -t openkyrozen .
docker run -p 8000:8000 -e DEEPSEEK_API_KEY=sk-... openkyrozen
docker run -p 8000:8000 \
-e DEEPSEEK_API_KEY=sk-... \
-e KYROZEN_SERVER_TOKEN=change-me \
-v kyrozen-data:/data \
openkyrozen
```

Web インターフェースは、リアルタイムストリーミング、コスト追跡、セッション管理を備えたダークテーマのチャット UI を提供します。
Expand Down Expand Up @@ -405,10 +409,14 @@ python server.py --port 8000
docker build -t openkyrozen .
docker run -p 8000:8000 \
-e DEEPSEEK_API_KEY=sk-your-key \
-v $(pwd)/chroma_memory:/app/chroma_memory \
-e KYROZEN_SERVER_TOKEN=change-me \
-e KYROZEN_DB_PATH=/data/openkyrozen.sqlite3 \
-v kyrozen-data:/data \
openkyrozen
```

イメージは非 root ユーザー `kyrozen` で実行されます。SQLite の事実上の保存先は `/data/openkyrozen.sqlite3`(イメージが `KYROZEN_DB_PATH` に設定)です。コンテナを置き換える場合も同じ名前付きボリュームを `/data` にマウントしてください。ローカルでは `make docker-smoke` で置き換え後の復元テストを実行できます。

---

## 🔌 プラグインシステム
Expand Down
12 changes: 10 additions & 2 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,11 @@ python server.py --port 8000

# 또는 Docker 사용:
docker build -t openkyrozen .
docker run -p 8000:8000 -e DEEPSEEK_API_KEY=sk-... openkyrozen
docker run -p 8000:8000 \
-e DEEPSEEK_API_KEY=sk-... \
-e KYROZEN_SERVER_TOKEN=change-me \
-v kyrozen-data:/data \
openkyrozen
```

웹 인터페이스는 실시간 스트리밍, 비용 추적, 세션 관리 기능을 갖춘 다크 테마 채팅 UI를 제공합니다.
Expand Down Expand Up @@ -405,10 +409,14 @@ python server.py --port 8000
docker build -t openkyrozen .
docker run -p 8000:8000 \
-e DEEPSEEK_API_KEY=sk-your-key \
-v $(pwd)/chroma_memory:/app/chroma_memory \
-e KYROZEN_SERVER_TOKEN=change-me \
-e KYROZEN_DB_PATH=/data/openkyrozen.sqlite3 \
-v kyrozen-data:/data \
openkyrozen
```

이미지는 root가 아닌 `kyrozen` 사용자로 실행됩니다. SQLite 원본 데이터는 `/data/openkyrozen.sqlite3`에 저장되며(이미지에서 `KYROZEN_DB_PATH`를 이 경로로 설정), 컨테이너를 교체할 때도 같은 이름의 볼륨을 `/data`에 마운트해야 합니다. 로컬에서는 `make docker-smoke`로 교체 후 복구 테스트를 실행할 수 있습니다.

---

## 🔌 플러그인 시스템
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ KYROZEN_SERVER_TOKEN=change-me python server.py --host 0.0.0.0 --port 8000

# Or via Docker:
docker build -t openkyrozen .
docker run -p 8000:8000 -e DEEPSEEK_API_KEY=sk-... -e KYROZEN_SERVER_TOKEN=change-me openkyrozen
docker run -p 8000:8000 \
-e DEEPSEEK_API_KEY=sk-... \
-e KYROZEN_SERVER_TOKEN=change-me \
-v kyrozen-data:/data \
openkyrozen
```

The web interface provides a dark-themed chat UI with real-time streaming, cost tracking, and session management.
Expand Down Expand Up @@ -612,10 +616,16 @@ docker build -t openkyrozen .
docker run -p 8000:8000 \
-e DEEPSEEK_API_KEY=sk-your-key \
-e KYROZEN_SERVER_TOKEN=change-me \
-v kyrozen-data:/root/.kyrozen/v2 \
-e KYROZEN_DB_PATH=/data/openkyrozen.sqlite3 \
-v kyrozen-data:/data \
openkyrozen
```

The image runs as the non-root `kyrozen` user. SQLite is the durable source of
truth at `/data/openkyrozen.sqlite3` (the image sets `KYROZEN_DB_PATH` to this
path), so keep the named volume mounted at `/data` when replacing containers.
To run the same replacement-and-recovery check locally, use `make docker-smoke`.

---

## 🔌 Plugin System
Expand Down
12 changes: 10 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ KYROZEN_SERVER_TOKEN=change-me python server.py --host 0.0.0.0 --port 8000

# 或通过 Docker:
docker build -t openkyrozen .
docker run -p 8000:8000 -e DEEPSEEK_API_KEY=sk-... -e KYROZEN_SERVER_TOKEN=change-me openkyrozen
docker run -p 8000:8000 \
-e DEEPSEEK_API_KEY=sk-... \
-e KYROZEN_SERVER_TOKEN=change-me \
-v kyrozen-data:/data \
openkyrozen
```

Web 界面提供暗色主题的聊天 UI,支持实时流式输出、费用追踪和会话管理。
Expand Down Expand Up @@ -447,10 +451,14 @@ docker build -t openkyrozen .
docker run -p 8000:8000 \
-e DEEPSEEK_API_KEY=sk-your-key \
-e KYROZEN_SERVER_TOKEN=change-me \
-v kyrozen-data:/root/.kyrozen/v2 \
-e KYROZEN_DB_PATH=/data/openkyrozen.sqlite3 \
-v kyrozen-data:/data \
openkyrozen
```

镜像使用非 root 用户 `kyrozen` 运行。SQLite 事实主库位于
`/data/openkyrozen.sqlite3`(镜像已将 `KYROZEN_DB_PATH` 设为该路径);替换容器时请继续将同一个命名卷挂载到 `/data`。本地可用 `make docker-smoke` 运行同样的替换容器并恢复记忆测试。

---

## 🔌 插件系统
Expand Down
94 changes: 94 additions & 0 deletions scripts/docker_persistence_smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env bash

set -Eeuo pipefail

IMAGE="${1:-openkyrozen-test}"
BASE_PORT="${DOCKER_SMOKE_BASE_PORT:-18080}"
PORT_A="${BASE_PORT}"
PORT_B="$((BASE_PORT + 1))"
RUN_SUFFIX="${GITHUB_RUN_ID:-local}-${RANDOM}-$$"
VOLUME_NAME="openkyrozen-smoke-${RUN_SUFFIX}"
CONTAINER_A="openkyrozen-smoke-a-${RUN_SUFFIX}"
CONTAINER_B="openkyrozen-smoke-b-${RUN_SUFFIX}"
TOKEN="openkyrozen-smoke-token"

require_command() {
command -v "$1" >/dev/null 2>&1 || {
echo "docker persistence smoke test requires '$1'" >&2
exit 2
}
}

cleanup() {
docker rm -f "$CONTAINER_A" "$CONTAINER_B" >/dev/null 2>&1 || true
docker volume rm "$VOLUME_NAME" >/dev/null 2>&1 || true
}

wait_for_server() {
local container="$1"
local port="$2"
local response

for _ in $(seq 1 90); do
if response="$(curl --silent --show-error --fail --max-time 3 \
-H "Authorization: Bearer ${TOKEN}" \
"http://127.0.0.1:${port}/api/health" 2>/dev/null)" \
&& python3 -c 'import json, sys; data = json.load(sys.stdin); raise SystemExit(0 if data.get("status") == "ok" and data.get("provider") == "ollama" else 1)' <<<"$response"; then
return 0
fi
sleep 1
done

echo "container did not become healthy: ${container}" >&2
docker logs "$container" >&2 || true
return 1
}

run_server() {
local container="$1"
local port="$2"

docker run --detach --name "$container" \
--publish "127.0.0.1:${port}:8000" \
--env KYROZEN_PROVIDER=ollama \
--env KYROZEN_SERVER_TOKEN="$TOKEN" \
--env KYROZEN_DB_PATH=/data/openkyrozen.sqlite3 \
--env KYROZEN_DISABLE_VECTOR_INDEX=1 \
--mount "type=volume,src=${VOLUME_NAME},dst=/data" \
"$IMAGE" >/dev/null
}

require_command docker
require_command curl
require_command python3
docker image inspect "$IMAGE" >/dev/null

trap cleanup EXIT INT TERM
docker volume create "$VOLUME_NAME" >/dev/null

run_server "$CONTAINER_A" "$PORT_A"
wait_for_server "$CONTAINER_A" "$PORT_A"

test "$(docker exec "$CONTAINER_A" id -u)" = "10001"
test "$(docker exec "$CONTAINER_A" sh -c 'test -w /data && stat -c %u /data')" = "10001"

claim_payload='{"key":"docker_persistence_smoke","value":"survives container replacement","kind":"fact","authority":"owner","scope":"global","claim_type":"general","visibility":"public"}'
created="$(curl --silent --show-error --fail \
-X POST "http://127.0.0.1:${PORT_A}/api/v2/memory/claims" \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Content-Type: application/json' \
--data "$claim_payload")"
memory_id="$(python3 -c 'import json, sys; value = json.load(sys.stdin).get("memory_id"); assert value; print(value)' <<<"$created")"
test -n "$memory_id"

docker exec "$CONTAINER_A" sh -c 'test -f /data/openkyrozen.sqlite3'
docker rm -f "$CONTAINER_A" >/dev/null

run_server "$CONTAINER_B" "$PORT_B"
wait_for_server "$CONTAINER_B" "$PORT_B"
recovered="$(curl --silent --show-error --fail \
"http://127.0.0.1:${PORT_B}/api/v2/memory/claims" \
-H "Authorization: Bearer ${TOKEN}")"
python3 -c 'import json, sys; data = json.load(sys.stdin); expected = sys.argv[1]; assert any(item.get("id") == expected for item in data.get("claims", []))' "$memory_id" <<<"$recovered"

echo "Docker persistence smoke passed: memory ${memory_id} survived container replacement as uid 10001."
Loading