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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ jobs:
pip install --quiet -r services/api/requirements.txt
pip install --quiet pytest httpx

- name: Lint
# Same pinned ruff and policy as Pi (ruff.toml).
run: |
pip install --quiet ruff==0.16.6
python -m ruff check .

- name: The module contract requires these files to exist
run: |
missing=""
Expand Down
40 changes: 40 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Lint settings for MemoryGate; the same policy as Pi's, shared across Conker modules.
#
# Explicit rather than default, because the defaults move between ruff releases
# and a lint set that changes under you produces diffs nobody asked for. Pinned
# in requirements-dev.txt for the same reason.

line-length = 100
target-version = "py311"

[lint]
select = [
"E", "W", # pycodestyle
"F", # pyflakes
"I", # import order
"UP", # modern syntax for the target version
"B", # bugbear
"SIM", # obvious simplifications
"ISC", # implicit string concatenation, which hides missing commas
"RUF",
]
ignore = [
# Health checks and status endpoints catch broadly on purpose. This service
# reports `unavailable` with a reason when a dependency misbehaves; a check
# that raised instead would take down the very endpoint the owner uses to
# find out what is wrong. See the truthful-status rule in CLAUDE.md.
"BLE001",
# Line length is left to review until this module adopts `ruff format`.
"E501",
]

[lint.per-file-ignores]
# Test doubles subclass stdlib handlers whose method names are fixed by the
# stdlib, and hold shared state as class attributes on purpose.
"tests/*" = ["N802", "RUF012", "F811"] # F811: pytest fixtures are imported, then requested by name
# Test doubles, fixture imports, and multilingual (Russian) sample text on purpose.
"services/api/tests/*" = ["N802", "RUF012", "F811", "RUF001"]
# Importing every model registers its table for create_all.
"services/api/app/models/__init__.py" = ["F401"]
# Russian vocabulary for multilingual signal detection (ADR-0004), not look-alike letters.
"services/api/app/services/signal_filter.py" = ["RUF001"]
10 changes: 8 additions & 2 deletions services/api/app/core/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from fastapi import Header, HTTPException, Request
from app.core.db import SessionLocal
from app.services.auth_settings_service import clear_failed_attempts, get_lockout_status, register_failed_attempt, verify_admin_key, verify_agent_access_key
from app.services.auth_settings_service import (
clear_failed_attempts,
get_lockout_status,
register_failed_attempt,
verify_admin_key,
verify_agent_access_key,
)
from fastapi import Header, HTTPException, Request


def require_key(request: Request, x_memorygate_key: str | None = Header(None, alias="X-MemoryGate-Key")) -> str:
Expand Down
2 changes: 1 addition & 1 deletion services/api/app/core/db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from app.core.config import DATABASE_URL
from sqlalchemy import create_engine, text
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from app.core.config import DATABASE_URL

engine = create_engine(DATABASE_URL, echo=False, pool_pre_ping=True)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
Expand Down
1 change: 1 addition & 0 deletions services/api/app/core/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""
import json
import uuid

from sqlalchemy import text
from sqlalchemy.engine import Engine

Expand Down
58 changes: 29 additions & 29 deletions services/api/app/main.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,46 @@
import logging
import os
import time
from datetime import datetime, timezone
from datetime import UTC, datetime

from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from app.core.auth import require_key
from app.core.config import CORS_ANY_ORIGIN, CORS_ORIGINS
from app.core.db import Base, database_health, engine
from app.core.migrations import run_migrations
from app.core.auth import require_key
from app.routes.memory import router as memory_router
from app.routes.audit import router as audit_router
from app.routes.entity import router as entity_router
from app.routes.observation import router as observation_router
from app.routes.pattern import router as pattern_router
from app.models import (
memory_forget, # noqa: F401 - registers the table for create_all
)
from app.routes.agent_config import router as agent_config_router
from app.routes.briefing import router as briefing_router
from app.routes.transcript import router as transcript_router
from app.routes.audit import router as audit_router
from app.routes.auth_settings import router as auth_settings_router
from app.routes.briefing import router as briefing_router
from app.routes.conversation import router as conversation_router
from app.routes.corrections import router as corrections_router
from app.routes.entity import router as entity_router
from app.routes.evidence import router as evidence_router
from app.routes.lineage import router as lineage_router
from app.routes.memory import router as memory_router
from app.routes.observation import router as observation_router
from app.routes.pattern import router as pattern_router
from app.routes.runtime import router as runtime_router
from app.routes.conversation import router as conversation_router
from app.routes.corrections import router as corrections_router
from app.routes.system import router as system_router
from app.routes.skills import context_router as skills_context_router
from app.routes.skills import router as skills_router
from app.models import memory, audit, agent_config
from app.models import memory_forget # noqa: F401 - registers the table for create_all
from app.models import auth_setting
from app.models import evidence_source, evidence_object, analysis_object
from app.models import episode_object, object_link
from app.models import processing_job
from app.models import entity
from app.models import observation
from app.models import pattern
from app.models import session_transcript
from app.models import ai_runtime_setting
from app.services.qdrant_store import ensure_qdrant_collection, ensure_observation_collection, ensure_entity_collection, qdrant_health
from app.routes.system import router as system_router
from app.routes.transcript import router as transcript_router
from app.services.auth_settings_service import (
assert_admin_key_configured,
ensure_bootstrap_agent_access_key,
)
from app.services.embeddings import embedding_health
from app.services.processing_worker import start_worker, stop_worker
from app.services.auth_settings_service import assert_admin_key_configured, ensure_bootstrap_agent_access_key
from app.services.qdrant_store import (
ensure_entity_collection,
ensure_observation_collection,
ensure_qdrant_collection,
qdrant_health,
)
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware

log = logging.getLogger("memorygate")

Expand Down Expand Up @@ -70,8 +70,8 @@
@app.on_event("startup")
def startup():
Base.metadata.create_all(bind=engine)
from app.services.deletion_recovery import assert_not_held
from app.core.db import SessionLocal
from app.services.deletion_recovery import assert_not_held
with SessionLocal() as recovery_db:
assert_not_held(recovery_db)
run_migrations(engine)
Expand Down Expand Up @@ -138,7 +138,7 @@ def health():
"status": "degraded" if degraded else "ok",
"degraded": degraded,
"checks": checks,
"checked_at": datetime.now(timezone.utc).isoformat(),
"checked_at": datetime.now(UTC).isoformat(),
}
_health_cache["result"] = result
_health_cache["checked_at"] = now
Expand Down
20 changes: 10 additions & 10 deletions services/api/app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from .memory import Memory
from .deletion_receipt import DeletionReceipt
from .audit import MemoryAudit
from .agent_access_key import AgentAccessKey
from .agent_config import AgentConfig
from .auth_setting import AuthSetting
from .evidence_source import EvidenceSource
from .evidence_object import EvidenceObject
from .ai_runtime_setting import AiRuntimeSetting
from .analysis_object import AnalysisObject
from .audit import MemoryAudit
from .auth_setting import AuthSetting
from .deletion_receipt import DeletionReceipt
from .episode_object import EpisodeObject
from .evidence_object import EvidenceObject
from .evidence_source import EvidenceSource
from .memory import Memory
from .memory_conflict import MemoryConflict
from .memory_revision import MemoryRevision
from .object_link import ObjectLink
from .processing_job import ProcessingJob
from .agent_access_key import AgentAccessKey
from .memory_revision import MemoryRevision
from .memory_conflict import MemoryConflict
from .ai_runtime_setting import AiRuntimeSetting
4 changes: 2 additions & 2 deletions services/api/app/models/agent_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from sqlalchemy import String, Float, Integer, Boolean, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base
from sqlalchemy import Boolean, DateTime, Float, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column


class AgentConfig(Base):
Expand Down
3 changes: 1 addition & 2 deletions services/api/app/models/ai_runtime_setting.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from datetime import datetime

from app.core.db import Base
from sqlalchemy import DateTime, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column

from app.core.db import Base


class AiRuntimeSetting(Base):
"""Singleton model-provider configuration; API keys remain encrypted at rest."""
Expand Down
7 changes: 4 additions & 3 deletions services/api/app/models/analysis_object.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from sqlalchemy import String, Text, DateTime, Float, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base
import uuid

from app.core.db import Base
from sqlalchemy import DateTime, Float, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column


class AnalysisObject(Base):
__tablename__ = "analysis_objects"
Expand Down
8 changes: 5 additions & 3 deletions services/api/app/models/audit.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from sqlalchemy import String, Text, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base
import uuid

from app.core.db import Base
from sqlalchemy import DateTime, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column


class MemoryAudit(Base):
__tablename__ = "memory_audit"

Expand Down
2 changes: 1 addition & 1 deletion services/api/app/models/auth_setting.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from app.core.db import Base
from sqlalchemy import String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base


class AuthSetting(Base):
Expand Down
7 changes: 3 additions & 4 deletions services/api/app/models/deletion_receipt.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
"""Content-free deletion identities retained for recovery replay."""
from datetime import datetime, timezone
from datetime import UTC, datetime

from app.core.db import Base
from sqlalchemy import DateTime, String
from sqlalchemy.orm import Mapped, mapped_column

from app.core.db import Base


class DeletionReceipt(Base):
__tablename__ = "deletion_receipts"
agent_id: Mapped[str] = mapped_column(String, primary_key=True)
object_kind: Mapped[str] = mapped_column(String, primary_key=True)
object_id: Mapped[str] = mapped_column(String, primary_key=True)
deleted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc))
default=lambda: datetime.now(UTC))


class RecoveryHold(Base):
Expand Down
8 changes: 5 additions & 3 deletions services/api/app/models/entity.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from sqlalchemy import String, Text, DateTime, Float, Boolean, Integer, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base
import uuid

from app.core.db import Base
from sqlalchemy import DateTime, Float, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column


class Entity(Base):
__tablename__ = "entities"

Expand Down
3 changes: 2 additions & 1 deletion services/api/app/models/episode_object.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import uuid

from app.core.db import Base
from sqlalchemy import DateTime, Float, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base


class EpisodeObject(Base):
Expand Down
7 changes: 4 additions & 3 deletions services/api/app/models/evidence_object.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from sqlalchemy import String, Text, DateTime, Float, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base
import uuid

from app.core.db import Base
from sqlalchemy import DateTime, Float, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column


class EvidenceObject(Base):
__tablename__ = "evidence_objects"
Expand Down
7 changes: 4 additions & 3 deletions services/api/app/models/evidence_source.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from sqlalchemy import String, Text, DateTime, Boolean, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base
import uuid

from app.core.db import Base
from sqlalchemy import Boolean, DateTime, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column


class EvidenceSource(Base):
__tablename__ = "evidence_sources"
Expand Down
10 changes: 6 additions & 4 deletions services/api/app/models/memory.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from sqlalchemy import String, Text, DateTime, Boolean, Integer, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base
import uuid

from app.core.db import Base
from sqlalchemy import Boolean, DateTime, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column


class Memory(Base):
__tablename__ = "memories"

id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
agent_id: Mapped[str] = mapped_column(String, default="default", index=True)
revision: Mapped[int] = mapped_column(Integer, nullable=False, default=1, server_default="1")
__mapper_args__ = {"version_id_col": revision}
__mapper_args__ = {"version_id_col": revision} # noqa: RUF012 - SQLAlchemy reads this class attribute

text: Mapped[str] = mapped_column(Text)
summary: Mapped[str] = mapped_column(Text, default="")
Expand Down
3 changes: 2 additions & 1 deletion services/api/app/models/memory_conflict.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import uuid

from app.core.db import Base
from sqlalchemy import DateTime, Float, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base


class MemoryConflict(Base):
Expand Down
3 changes: 2 additions & 1 deletion services/api/app/models/memory_revision.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import uuid

from app.core.db import Base
from sqlalchemy import DateTime, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base


class MemoryRevision(Base):
Expand Down
3 changes: 2 additions & 1 deletion services/api/app/models/object_link.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import uuid

from app.core.db import Base
from sqlalchemy import DateTime, Float, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base


class ObjectLink(Base):
Expand Down
8 changes: 5 additions & 3 deletions services/api/app/models/observation.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from sqlalchemy import String, Text, DateTime, Float, Integer, Boolean, func
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base
import uuid

from app.core.db import Base
from sqlalchemy import Boolean, DateTime, Float, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column


class Observation(Base):
__tablename__ = "observations"

Expand Down
Loading
Loading