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
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore(deps)"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore(deps)"
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,33 @@ jobs:

sbom:
uses: LegionForge/dev-rig/.github/workflows/sbom.yml@main

integration:
name: Integration (live Postgres)
runs-on: ubuntu-latest
services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_PASSWORD: ci-throwaway
POSTGRES_DB: jeli_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
JELI_TEST_DB_URL: postgresql://postgres:[email protected]:5432/jeli_test
SCOPED_MCP_DB_URL: postgresql://postgres:[email protected]:5432/jeli_test
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
- run: pip install -e ".[dev]" sqlalchemy psycopg2-binary
- run: python -m alembic upgrade head
- run: python -m pytest tests/integration/ -v --no-cov
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Changelog

## v0.1.0-alpha — 2026-07-02

First working release: a cryptographically auditable memory store with a
scoped agent API.

### Added
- Scoped MCP server (stdio) exposing exactly four tools: `capture_memory`,
`search_memory` (fts), `audit_trail`, `verify_chain`
- HMAC-SHA256 hash chain with per-record signing-key identity (`key_id`
inside the canonical hashed form; rotation without re-signing history;
unknown keys fail closed)
- Trust-scored writes (user-stated 1.0 … external 0.3); injection-styled
content capped at 0.3 and flagged, at write time and in search results
- Server-side actor identity (agents cannot impersonate writers)
- Chain writes serialized under a Postgres advisory lock (concurrent
multi-agent writers cannot fork the chain)
- Append-only enforced at the DB privilege layer (`jeli_app` role:
INSERT+SELECT only — scripts/setup_db_roles.sql)
- `jeli verify` CLI (exit 0 valid / 1 broken / 2 misconfigured, `--json`)
- Alembic schema (memory_entry, memory_audit_log, memory_contradiction),
embeddings stored as JSONB with full provenance (model, dims, timestamp)
- Threat model documenting guarantees and known gaps (docs/THREAT-MODEL.md)
- CI via dev-rig reusable workflows + live-Postgres integration job;
pre-push history-scrub hook (.githooks/pre-push)

### Known limitations (see THREAT-MODEL.md)
- Semantic search lands with the pgvector migration; fts (substring) only
- Temporal invalidation fields are not yet integrity-protected
- Poisoning at write time is flagged/audited, not prevented
19 changes: 19 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Security Policy

Jeli is a security and governance layer for personal memory systems; reports
about its own security are taken seriously and handled quickly.

## Reporting a vulnerability

- Preferred: [GitHub private vulnerability reporting](https://github.com/LegionForge/jeli/security/advisories/new)
- Or email: [email protected]

Please include reproduction steps and affected version/commit. You should
receive an acknowledgement within 72 hours.

## Scope notes

- Known, documented limitations of the v0.1 integrity model are listed in
[docs/THREAT-MODEL.md](docs/THREAT-MODEL.md) — reports that reduce to those
documented gaps are still welcome, especially with novel exploitation paths.
- Supported version: latest release on `main`.
Binary file modified alembic/__pycache__/env.cpython-311.pyc
Binary file not shown.
6 changes: 5 additions & 1 deletion alembic/versions/001_initial_jeli_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ def upgrade() -> None:
)

# Indices for memory_audit_log
op.create_index("idx_audit_memory", "memory_audit_log", ["memory_id", "timestamp"], postgresql_using="DESC")
op.create_index(
"idx_audit_memory",
"memory_audit_log",
["memory_id", sa.text("timestamp DESC")],
)

# memory_contradiction — Unresolved contradictions (flags for Judicial layer)
op.create_table(
Expand Down
50 changes: 50 additions & 0 deletions docs/THREAT-MODEL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Jeli Threat Model — v0.1

Honest statement of what the v0.1 integrity layer does and does not guarantee.
Overclaiming is worse than the gap: agents and users will calibrate their
trust to this document.

## What v0.1 guarantees

| Property | Mechanism |
|---|---|
| Post-write content tampering is detectable | HMAC-SHA256 hash chain over the canonical record (`jeli verify` finds the first bad record) |
| Records cannot be silently reordered or removed mid-chain | prev-hash linkage breaks on any resequencing |
| A record cannot be re-pointed at a weaker signing key | `key_id` is inside the hashed canonical form |
| Key rotation without re-signing history | per-record key registry; unknown `key_id` fails closed |
| Agents cannot impersonate other writers | actor identity is server-side config, not a tool argument |
| Injection-styled content cannot claim authority | write-path pattern match caps trust at 0.3 and flags it; the flag is returned at read time |
| History cannot be rewritten by the app itself | `jeli_app` DB role holds INSERT+SELECT only (scripts/setup_db_roles.sql) |
| Concurrent writers cannot fork the chain | chain writes serialize under a Postgres advisory lock |

## What v0.1 does NOT guarantee — known gaps

**Poisoning at write time is flagged, not prevented.** A MINJA-style attack
writes through the legitimate path and receives a perfectly valid hash. The
defenses are heuristic (pattern flagging, trust capping, provenance for later
revocation) — not cryptographic. Jeli v0.1 is *poison-auditable*, not
poison-proof. Collusive multi-record poisoning and consolidation-skew
(flooding) attacks are out of scope for v0.1 entirely.

**Temporal fields are not integrity-protected.** `valid_until`,
`superseded_by`, and `amended_from` are set after write, so they cannot live
inside the write-time hash. An attacker with admin DB access can resurrect a
retracted memory or hide a live one without breaking any hash. Planned fix:
supersession/amendment become hash-chained append events (the audit trail
becomes the authority for state). Until then, mitigations are the restricted
`jeli_app` role (the app cannot do it) and DB-access hygiene.

**Chain-key compromise defeats verification.** An attacker holding both DB
write access and the chain key can rewrite and re-sign everything. Planned
fix: keys held in a vault (OpenBAO transit — Jeli requests signatures, never
holds key material) plus periodically anchored chain-head checkpoints stored
outside the database's blast radius. Until then: the chain key is a
root-grade credential; do not keep it in `.env` files on shared machines.

**The audit log is append-only by grants, not by cryptography.** Audit rows
are not yet hash-chained; an admin-level attacker can delete them silently.

**Search results are a prompt-injection channel.** `search_memory` returns
memory content into an agent's context. Consumers MUST treat results as
untrusted data, not instructions — the `injection_flagged` field exists so
callers can quarantine, but unflagged content is not certified safe.
35 changes: 35 additions & 0 deletions scripts/setup_db_roles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
-- Jeli append-only enforcement — at the privilege layer, not in code.
--
-- The MCP server connects as jeli_app, which structurally CANNOT update or
-- delete memory or audit rows. Even a fully compromised Jeli process cannot
-- rewrite history. Run once as a superuser/owner after `alembic upgrade head`:
--
-- psql -d jeli -f scripts/setup_db_roles.sql
--
-- Then point SCOPED_MCP_DB_URL at jeli_app. Migrations keep running as the
-- owning (admin) role, never as jeli_app.
--
-- Note: temporal invalidation (valid_until / superseded_by) is designed as an
-- UPDATE and is therefore NOT grantable to jeli_app; in Phase 1 those fields
-- are set by the admin role only. See docs/THREAT-MODEL.md §"Temporal fields".

DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'jeli_app') THEN
CREATE ROLE jeli_app LOGIN;
END IF;
END
$$;

-- \password jeli_app -- set interactively; never store the password in SQL

REVOKE ALL ON memory_entry, memory_audit_log, memory_contradiction FROM jeli_app;

GRANT SELECT, INSERT ON memory_entry TO jeli_app;
GRANT SELECT, INSERT ON memory_audit_log TO jeli_app;
GRANT SELECT, INSERT ON memory_contradiction TO jeli_app;

-- audit log id is BIGSERIAL; INSERT needs the sequence
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO jeli_app;

-- explicitly no UPDATE, no DELETE, no TRUNCATE, anywhere.
Binary file modified src/jeli_scoped_mcp/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file modified src/jeli_scoped_mcp/__pycache__/config.cpython-311.pyc
Binary file not shown.
Binary file modified src/jeli_scoped_mcp/__pycache__/security.cpython-311.pyc
Binary file not shown.
7 changes: 5 additions & 2 deletions src/jeli_scoped_mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ class Config:

def validate_required(self):
"""Validate that all required settings are present."""
if not self.api_key:
raise ValueError("SCOPED_MCP_API_KEY is required")
# stdio's auth boundary is process spawn; the API key guards the
# (future) HTTP transport only — requiring it for stdio would be
# security theater.
if self.transport == "http" and not self.api_key:
raise ValueError("SCOPED_MCP_API_KEY is required for http transport")
if not self.chain_key:
raise ValueError("SCOPED_MCP_CHAIN_KEY is required (hash-chain HMAC key)")
if self.embedding_provider == "openai" and not self.openai_api_key:
Expand Down
Binary file modified src/jeli_scoped_mcp/core/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file not shown.
Binary file modified src/jeli_scoped_mcp/core/__pycache__/hash_chain.cpython-311.pyc
Binary file not shown.
Binary file modified src/jeli_scoped_mcp/core/__pycache__/trust_score.cpython-311.pyc
Binary file not shown.
16 changes: 16 additions & 0 deletions src/jeli_scoped_mcp/database/pool.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Async PostgreSQL connection pooling via asyncpg."""

from contextlib import asynccontextmanager
from typing import Any

import asyncpg
Expand Down Expand Up @@ -58,6 +59,21 @@ async def fetchval(self, query: str, *args) -> Any:
async with self.pool.acquire() as conn:
return await conn.fetchval(query, *args)

@asynccontextmanager
async def locked_transaction(self, lock_key: int):
"""One transaction holding a pg advisory lock — serializes chain writes.

The prev-hash read and the insert must be atomic or two concurrent
writers fork the chain (same prev_hash twice) and verify_chain flags
legitimate data as tampered.
"""
if not self.pool:
raise RuntimeError("Connection pool not initialized")
async with self.pool.acquire() as conn:
async with conn.transaction():
await conn.execute("SELECT pg_advisory_xact_lock($1)", lock_key)
yield conn

async def health_check(self) -> bool:
"""Check if pool is healthy."""
try:
Expand Down
Loading