diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 0dc0087..82ee750 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -8,6 +8,8 @@ on: jobs: test: runs-on: ubuntu-latest + permissions: + contents: read services: postgres: image: pgvector/pgvector:pg16 @@ -70,3 +72,70 @@ jobs: /tmp/openbrain-smoke/bin/openbrain install-hermes --hermes-home /tmp/hermes-smoke test -f /tmp/hermes-smoke/plugins/openbrain/plugin.yaml test -f /tmp/hermes-smoke/plugins/openbrain/__init__.py + + release: + needs: test + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install build tooling + run: python -m pip install --upgrade build + - name: Read package version + id: version + shell: bash + run: | + VERSION="$(python - <<'PY' + import tomllib + from pathlib import Path + + project = tomllib.loads(Path('pyproject.toml').read_text(encoding='utf-8'))['project'] + print(project['version']) + PY + )" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + - name: Check whether release already exists + id: existing + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.version.outputs.tag }} + shell: bash + run: | + if gh release view "$TAG" >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + - name: Build package + if: steps.existing.outputs.exists == 'false' + run: python -m build + - name: Smoke-test release wheel + if: steps.existing.outputs.exists == 'false' + env: + EXPECTED_VERSION: ${{ steps.version.outputs.version }} + shell: bash + run: | + python -m venv /tmp/openbrain-release-smoke + /tmp/openbrain-release-smoke/bin/pip install dist/*.whl + ACTUAL_VERSION="$(/tmp/openbrain-release-smoke/bin/openbrain --version | awk '{print $3}')" + test "$ACTUAL_VERSION" = "$EXPECTED_VERSION" + - name: Publish GitHub release + if: steps.existing.outputs.exists == 'false' + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.version.outputs.tag }} + VERSION: ${{ steps.version.outputs.version }} + shell: bash + run: | + gh release create "$TAG" dist/*.whl dist/*.tar.gz \ + --target "$GITHUB_SHA" \ + --title "Open Brain $VERSION" \ + --generate-notes diff --git a/CHANGELOG.md b/CHANGELOG.md index 99dde31..1160c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,21 +4,38 @@ All notable changes to Open Brain are documented in this file. The project follo ## [Unreleased] +## [1.0.2] - 2026-09-07 + +### Fixed +- Capped the MCP Python SDK dependency to `mcp>=1.0.0,<2.0.0`, preventing fresh installs from resolving the incompatible 2.x API while Open Brain still uses the 1.x server decorators. +- Added bounded chunking, pooling, and validation for oversized embedding inputs so provider context limits no longer make memory storage fail outright. +- Added shared embedding regeneration for REST and MCP, with safe NULL-only replacement by default and an explicit force mode for embedding-model migrations. +- Made migration `015_embedding_dim_change.sql` safe on databases where the legacy `memory` table is not present yet. +- Added `001_base_schema.sql` to the packaged migration chain so an empty PostgreSQL + pgvector database can be initialized from a pip/pipx install. +- Added `openbrain migrate` as the supported installed-package database bootstrap/update command. +- Fixed e2e pytest collection so the standalone API runner skips cleanly when the API stack is unavailable instead of aborting the full test suite during module import. + ### Changed -- Default docker-compose stack now ships an `ollama` service running the `nomic-embed-text` model (768-dim). The api no longer requires an external embeddings API key for a working local stack. -- Default API host port changed from `8000` to `8765`. Port 8000 is held by the Windows IP Helper service (`iphlpsvc`) on many Windows hosts, which prevents the api container from binding. Override with `API_PORT` in `.env`. -- `scripts/setup_db.py` now honours `DB_HOST`/`DB_PORT`/`DB_NAME`/`DB_USER` environment variables, matching the pattern used by `check_db.py`. The api container's compose-injected values (e.g. `postgres:5432`) now apply correctly on first run. -- `scripts/startup.sh` now invokes `scripts/migrate.py` after `setup_db.py`. The README always claimed the API applied migrations on startup; the code now matches the documentation. -- `src/db/connection.py` registers `psycopg2.extras.UUID_adapter` at import time. Queries that bind `uuid.UUID` parameters (`get_memory_by_id`, etc.) no longer raise `ProgrammingError: can't adapt type 'UUID'`. +- Documented the standalone PostgreSQL + pgvector bootstrap flow in the README and installation guide. +- Aligned package and Hermes plugin release metadata on version `1.0.2`. +- Added release automation that publishes a tagged GitHub release with wheel and source-distribution artifacts only after `Verify` succeeds on `master`. -### Added -- `scripts/quickstart.sh` brings up the docker stack, waits for the api to become healthy, ensures the embedding model is available, and (with `--with-hermes`) wires Open Brain into a locally-installed Hermes as the active memory provider. Idempotent. -- Migration `015_embedding_dim_change.sql` alters `memory.embedding` from `vector(1536)` to `vector(768)` and rebuilds the HNSW index to match the new Ollama-backed default embedder. -- `.gitattributes` forces LF line endings for `*.sh`, `*.py`, `*.sql`, and similar source files so Windows checkouts no longer reintroduce CRLF into shell scripts. -- `.gitignore` now excludes `.env`; `.env.example` is the single source of truth for non-secret defaults. +## [1.0.1] - 2026-07-24 + +### Fixed +- Made the local Docker stack work from a fresh clone by enforcing LF shell-script line endings, honoring database environment variables, running migrations during API startup, and registering psycopg2 UUID adaptation. +- Updated NLTK resource handling for the 3.8.2+ resource-name changes and baked the required data into the API image. -### Security -- Removed the previously-checked-in `.env` from version control. Operators should `cp .env.example .env` (or let `openbrain configure --project-root .` generate the secrets) before first run. +### Changed +- Switched the default local embedding service to Ollama with `nomic-embed-text` (768 dimensions), removing the need for an external embeddings API key for the default stack. +- Changed the default host API port from `8000` to `8765` to avoid common Windows port conflicts. +- Added migration `015_embedding_dim_change.sql` to align `memory.embedding` with the 768-dimensional default model and rebuild the HNSW index. +- Removed the checked-in `.env`; `.env.example` is now the non-secret source of defaults and generated credentials remain private. +- Added `.gitattributes` so Windows checkouts do not reintroduce CRLF into scripts and source files. + +### Added +- Added `scripts/quickstart.sh` for idempotent local stack bootstrap and optional Hermes wiring. +- Added the API e2e suite covering health, memory, continuity, context, review workflows, maintenance, imports, and authentication behavior. ## [1.0.0] - 2026-07-22 @@ -51,4 +68,6 @@ All notable changes to Open Brain are documented in this file. The project follo A production deployment is ready only after automatic checks pass and operators explicitly attest that TLS, backups, restore drills, monitoring, and migration records have been verified. Open Brain deliberately does not infer these external controls from configuration alone. +[1.0.2]: https://github.com/benclawbot/open-brain/releases/tag/v1.0.2 +[1.0.1]: https://github.com/benclawbot/open-brain/releases/tag/v1.0.1 [1.0.0]: https://github.com/benclawbot/open-brain/releases/tag/v1.0.0 diff --git a/README.md b/README.md index aa1b51e..c3dd2b1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Verify](https://github.com/benclawbot/open-brain/actions/workflows/verify.yml/badge.svg)](https://github.com/benclawbot/open-brain/actions/workflows/verify.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/license/mit) -[![Version](https://img.shields.io/badge/version-1.0.0-blue)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-1.0.2-blue)](CHANGELOG.md) ## Overview @@ -124,6 +124,14 @@ curl -fsSL https://raw.githubusercontent.com/benclawbot/open-brain/master/instal The installer uses `pipx`, keeping Open Brain isolated from system Python packages. It generates a private OpenBrain API key once in `~/.config/openbrain/.env`; upgrades reuse that key. This path is intended for users who already have a Postgres+pgvector instance elsewhere — the docker stack on this repo is the supported local-stack path. +Point `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, and `DB_PASSWORD` at that database, then initialize it directly from the installed package: + +```bash +openbrain migrate +``` + +The packaged migration chain starts at `001_base_schema.sql`, so a clean pipx install can create the core schema without cloning the repository or manually downloading `src/db/schema.sql`. + Verify: ```bash @@ -213,6 +221,7 @@ openbrain stats openbrain import file ./notes.md openbrain report --days 7 openbrain serve --host 127.0.0.1 --port 8000 +openbrain migrate openbrain install-hermes openbrain update ``` @@ -345,7 +354,7 @@ src/ ## Release status -**Open Brain 1.0.0 is the first production/stable release.** The coordinated production-readiness train delivered deployment hardening, database resilience, real adapter host wiring, durable reconciliation, staged imports, unified proposal review workflows, and a machine-readable release gate. +**Open Brain 1.0.2 is the current production/stable release.** This patch includes embedding resilience and recovery, MCP SDK compatibility, packaged database bootstrap for pip/pipx installs, and release automation that publishes verified wheel and source-distribution artifacts. Production readiness still depends on the target environment. Operators must run the readiness gate and verify external controls such as TLS, backups, restore drills, and monitoring before serving real data. @@ -353,4 +362,4 @@ See [`CHANGELOG.md`](CHANGELOG.md) for release details. ## License -MIT. +MIT. \ No newline at end of file diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 403dc29..2296fa0 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -10,6 +10,14 @@ The installer verifies Python 3.11+, installs or upgrades Open Brain through `pi Set `OPENBRAIN_INSTALL_HERMES=0` to skip automatic Hermes wiring. Set `OPENBRAIN_REPO_URL` to install from a fork. +For a standalone CLI install backed by an existing PostgreSQL + pgvector server, configure the database connection through `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, and `DB_PASSWORD`, then bootstrap the schema from the installed package: + +```sh +openbrain migrate +``` + +The migration chain now begins with `001_base_schema.sql`, so an empty database can be initialized without cloning the repository or locating `src/db/schema.sql` manually. The command is idempotent and uses the same checksum-protected migration ledger as `openbrain update`. + ## Hermes Automatic installation copies the packaged provider into `${HERMES_HOME:-~/.hermes}/plugins/openbrain`. Manual repair is available with: @@ -38,7 +46,7 @@ openbrain version-check openbrain update ``` -`version-check` is offline-safe. `update` upgrades the pipx installation and applies additive migrations. When package upgrade succeeds but migration fails, it exits non-zero and reports that existing data was not deleted. +`version-check` is offline-safe. `update` upgrades the pipx installation and applies additive migrations. When package upgrade succeeds but migration fails, it exits non-zero and reports that existing data was not deleted. You can also run `openbrain migrate` directly whenever you need to initialize or reconcile the database schema without upgrading the package. ## Diagnostics diff --git a/pyproject.toml b/pyproject.toml index c62cab2..02e12bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openbrain" -version = "1.0.0" +version = "1.0.2" description = "Shared personal memory, agent continuity, and actionable context" readme = "README.md" requires-python = ">=3.11" diff --git a/src/cli/__init__.py b/src/cli/__init__.py index 2741d3f..386ca0d 100644 --- a/src/cli/__init__.py +++ b/src/cli/__init__.py @@ -23,6 +23,21 @@ def _run(command: list[str]) -> None: subprocess.run(command, check=True) +def migrate_cmd() -> int: + """Apply packaged database migrations, including the base schema.""" + try: + from src.db.migrate import apply_migrations + applied = apply_migrations() + except Exception as exc: + print(f"Database migration failed: {exc}", file=sys.stderr) + return 1 + if applied: + print("Applied migrations: " + ", ".join(applied)) + else: + print("Database schema is up to date.") + return 0 + + def update_cmd(skip_migrations: bool = False) -> int: """Upgrade a pipx-managed installation and apply additive migrations.""" try: @@ -106,6 +121,8 @@ def main() -> int: serve_parser.add_argument("--port", "-p", type=int, default=8000) serve_parser.add_argument("--reload", action="store_true") + subparsers.add_parser("migrate", help="Create or update the database schema") + update_parser = subparsers.add_parser("update", help="Upgrade Open Brain and apply additive migrations") update_parser.add_argument("--skip-migrations", action="store_true") @@ -157,6 +174,8 @@ def main() -> int: if args.command == "serve": from .serve import serve_cmd return serve_cmd(args) + if args.command == "migrate": + return migrate_cmd() if args.command == "update": return update_cmd(skip_migrations=args.skip_migrations) if args.command == "configure": diff --git a/src/db/migrations/001_base_schema.sql b/src/db/migrations/001_base_schema.sql new file mode 100644 index 0000000..442e9d6 --- /dev/null +++ b/src/db/migrations/001_base_schema.sql @@ -0,0 +1,79 @@ +-- Open Brain Database Schema +-- PostgreSQL + pgvector + +-- Enable extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "vector"; + +-- Core memory table +CREATE TABLE IF NOT EXISTS memory ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + source VARCHAR(50) NOT NULL, + source_id VARCHAR(255), + captured_by TEXT, + content TEXT NOT NULL, + raw_content TEXT, + embedding vector(768), + entities JSONB DEFAULT '{}', + tags TEXT[] DEFAULT '{}', + tag_sources JSONB DEFAULT '{}', + importance FLOAT DEFAULT 0.5, + created_at TIMESTAMP DEFAULT NOW(), + original_date TIMESTAMP, + language VARCHAR(10), + metadata JSONB DEFAULT '{}' +); + +-- Indexes for columns present in the original base schema. The captured_by +-- index is intentionally left to 014_agent_attribution.sql so an older +-- existing memory table can reach the migration that adds captured_by first. +CREATE INDEX IF NOT EXISTS idx_memory_embedding ON memory USING hnsw (embedding vector_cosine_ops); +CREATE INDEX IF NOT EXISTS idx_memory_entities ON memory USING gin (entities); +CREATE INDEX IF NOT EXISTS idx_memory_tags ON memory USING gin (tags); +CREATE INDEX IF NOT EXISTS idx_memory_source ON memory (source); +CREATE INDEX IF NOT EXISTS idx_memory_created ON memory (created_at DESC); +CREATE INDEX IF NOT EXISTS idx_memory_original_date ON memory (original_date DESC); + +-- Full-text search index +CREATE INDEX IF NOT EXISTS idx_memory_content_fts ON memory USING GIN (to_tsvector('english', content)); + +-- Function to get memories from today +CREATE OR REPLACE FUNCTION get_today_memories(limit_count INTEGER DEFAULT 10) +RETURNS TABLE ( + id UUID, + source VARCHAR, + content TEXT, + tags TEXT[], + created_at TIMESTAMP, + importance FLOAT +) AS $$ +BEGIN + RETURN QUERY + SELECT m.id, m.source, m.content, m.tags, m.created_at, m.importance + FROM memory m + WHERE m.created_at >= CURRENT_DATE + ORDER BY m.importance DESC, m.created_at DESC + LIMIT limit_count; +END; +$$ LANGUAGE plpgsql; + +-- Function to get memory statistics +CREATE OR REPLACE FUNCTION get_memory_stats() +RETURNS TABLE ( + total_count BIGINT, + source_name VARCHAR, + source_count BIGINT, + tag_name TEXT, + tag_count BIGINT +) AS $$ +BEGIN + RETURN QUERY + SELECT + COUNT(*)::BIGINT as total_count, + NULL::VARCHAR as source_name, + NULL::BIGINT as source_count, + NULL::TEXT as tag_name, + NULL::BIGINT as tag_count + FROM memory; +END; +$$ LANGUAGE plpgsql; diff --git a/src/openbrain_hermes_plugin/plugin.yaml b/src/openbrain_hermes_plugin/plugin.yaml index 4e8f548..aa7f75a 100644 --- a/src/openbrain_hermes_plugin/plugin.yaml +++ b/src/openbrain_hermes_plugin/plugin.yaml @@ -1,5 +1,5 @@ name: openbrain -version: 1.0.0 +version: 1.0.2 description: Shared personal memory and cross-agent continuity through Open Brain hooks: - on_session_end diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 782d5c7..d2e62ab 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -16,6 +16,11 @@ import pytest +# test_api.py distinguishes pytest from direct script execution using this +# environment marker. Pytest normally sets it only while running a test, which +# is too late for module collection; set a harmless collection value here. +os.environ.setdefault("PYTEST_CURRENT_TEST", "collection") + def _api_url() -> str: return os.environ.get("OPENBRAIN_E2E_URL", "http://127.0.0.1:8765").rstrip("/") diff --git a/tests/test_embedding_resilience.py b/tests/test_embedding_resilience.py index f9aa491..e203d1d 100644 --- a/tests/test_embedding_resilience.py +++ b/tests/test_embedding_resilience.py @@ -161,7 +161,7 @@ def test_rest_and_mcp_expose_regeneration_controls(): assert EmbeddingRegenerationRequest(force=True).force is True assert any( - route.path == "/memories/{memory_id}/regenerate-embedding" + getattr(route, "path", None) == "/memories/{memory_id}/regenerate-embedding" for route in app.routes ) diff --git a/tests/test_schema_bootstrap.py b/tests/test_schema_bootstrap.py new file mode 100644 index 0000000..551274f --- /dev/null +++ b/tests/test_schema_bootstrap.py @@ -0,0 +1,42 @@ +from importlib.resources import files + + +def test_base_schema_is_packaged_as_first_migration() -> None: + migration_root = files("src.db.migrations") + base_sql = migration_root.joinpath("001_base_schema.sql").read_text(encoding="utf-8") + attribution_sql = migration_root.joinpath("014_agent_attribution.sql").read_text(encoding="utf-8") + + assert "CREATE TABLE IF NOT EXISTS memory" in base_sql + assert "CREATE EXTENSION IF NOT EXISTS \"vector\"" in base_sql + assert "idx_memory_captured_by" not in base_sql + assert "ADD COLUMN IF NOT EXISTS captured_by" in attribution_sql + assert "idx_memory_captured_by" in attribution_sql + + +def test_migrate_command_applies_packaged_migrations(monkeypatch, capsys) -> None: + from src.cli import migrate_cmd + from src.db import migrate + + monkeypatch.setattr( + migrate, + "apply_migrations", + lambda: ["001_base_schema.sql", "002_continuity_foundation.sql"], + ) + + assert migrate_cmd() == 0 + output = capsys.readouterr().out + assert "001_base_schema.sql" in output + assert "002_continuity_foundation.sql" in output + + +def test_migrate_command_reports_failure(monkeypatch, capsys) -> None: + from src.cli import migrate_cmd + from src.db import migrate + + def fail() -> list[str]: + raise RuntimeError("database unavailable") + + monkeypatch.setattr(migrate, "apply_migrations", fail) + + assert migrate_cmd() == 1 + assert "database unavailable" in capsys.readouterr().err