Changelog¶
luplo is pre-1.0 and tracks its changes via Alembic migration ids that
align with sprint versions. This page is the narrative layer on top of
the migrations in src/luplo/_db_assets/migrations/.
For the schema delta of any version, read the matching 0NNN_*.py file
in that directory.
v0.7.0 — luplo goes dry¶
Migration: 0006_drop_auth
luplo commits to being a library. Built-in authentication is removed; attribution is preserved. A deployment that needs users / orgs / OAuth wraps luplo as a library — it does not extend luplo’s schema.
Schema¶
actors— droppassword_hash,is_admin,last_login_at,oauth_provider,oauth_subject. The table now carries only attribution metadata:{id, name, email, role, external_ids, joined_at}. All ten FK columns that referenceactorsstay intact, so no item / history / audit / work-unit / glossary data is lost.auth_reset_tokens— dropped.
Surface — removed¶
src/luplo/server/auth/— 641 LOC across 10 files (JWT issuance, OAuth + PKCE for GitHub and Google, argon2id password hashing, magic-link reset flow, admin seed, email sender, domain filter)./auth/*endpoints (/login,/logout,/whoami,/token/refresh,/oauth/{provider}/start,/oauth/{provider}/callback,/reset-request,/reset-confirm).CLI commands:
lp login,lp logout,lp whoami,lp token refresh,lp admin set-password,lp server init-secrets,lp server config-check.Keyring-based JWT storage on the client.
Env vars:
LUPLO_JWT_SECRET/_ALG/_TTL_MINUTES,LUPLO_ADMIN_EMAIL/_PASSWORD_INITIAL,LUPLO_GITHUB_CLIENT_ID/_SECRET,LUPLO_GOOGLE_CLIENT_ID/_SECRET,LUPLO_SESSION_SECRET,LUPLO_ALLOWED_EMAIL_DOMAINS,LUPLO_AUTO_CREATE_USERS,LUPLO_AUTH_DISABLED.Runtime dependencies:
authlib,pyjwt,argon2-cffi,jinja2,itsdangerous(from[server]);keyring(from core).
Surface — added¶
GET /ready— readiness probe that round-tripsSELECT 1through the pool. Distinct from/health, which only reports that the process is up.X-Actorrequest header — every write handler resolves the attribution actor via the header first, then falls back to the newLUPLO_DEFAULT_ACTOR_IDserver setting. HTTP 400 when neither is present. Reads do not require it.
Migration guide¶
Run
alembic upgrade head. The existingactorsrows keep their identity metadata; only the deprecated auth columns disappear.If you ran the server with
LUPLO_AUTH_DISABLED=1, drop that env var — there is nothing to disable.If you ran real auth (JWT + cookies + OAuth), put a reverse proxy in front of luplo that authenticates users and forwards
X-Actor: <uuid>downstream. For multi-tenant deployments, wrap luplo as a library (from luplo.core.backend.local import LocalBackend) and let your own app own identity.CLI on the local backend: nothing to change.
lp initstill writes.luplowithactor.id; everylpcommand reads from it unchanged.
v0.6 — Audit, five refusals, password reset¶
Migration: 0005_auth_reset_tokens
Schema delta is small — one new table for magic-link password reset tokens. The bulk of v0.6 is new surface and a public identity rewrite, none of which touches the schema.
Schema¶
auth_reset_tokens—token_hash TEXT PRIMARY KEY,actor_id UUID REFERENCES actors(id) ON DELETE CASCADE,expires_at,used_at NULLABLE. Single-use, 15-minute TTL enforced in the handler. Argon2id hashes only; plaintext never touches disk or logs.
Surface¶
Audit (
lp impact,luplo_impact,GET /items/{id}/impact) — recursive CTE over typed edges (depends/blocks/supersedes/conflicts), five-hop ceiling enforced server-side, cycle prevention via path array, project-scoped, soft-delete-aware. Tree / flat / JSON output formats share the same structured payload across all three surfaces.lp task edit— edit a task’s title / body / sort_order via supersede. Status machine preserved; a typo on a done task can still be fixed. MCP toolluplo_task_editmirrors.On
task done, propose a decision draft — pure-read helpersuggest_decision_from_taskreturns anItemCreatedraft (never inserted). CLI flag--propose-decision; MCPluplo_task_donegainspropose_decision=False. ReturnsNonewhen the task lacks body/summary — honest silence over hallucinated template.Web-search-style query dialect —
"exact phrase"via<->,ORdisjunction,-negationvia!. Glossary expansion applies only to required and OR-group terms; phrases and negations pass through literally (expanding a negation would silently re-include the excluded concept).Password reset —
POST /auth/reset-request(always 200, no enumeration),POST /auth/reset-confirm(generic failure string, never leaks why).EmailSenderprotocol with logging / SMTP backends; transactional services (SES, Postmark) plug in later by implementing the protocol.Mutator project scope — 11 task/qa mutators gained optional
project_idkwarg, threaded into prefix resolution. Closes the v0.5 residual risk of a short prefix silently mutating a row in another project.
Docs¶
Philosophy restructured around five refusals (vectors-don’t-lead, five-hop, decisions-immutable, typed-and-bounded-edges, not-a-general-memory); the three operational commitments moved below as the enforcement layer.
New
docs/concepts/positioning.md— 8-axis comparison table against “a generic AI-memory tool”.New
docs/project/roadmap.md— sanitized public roadmap (Audit →/archive→ Notion webhook → rule pack).Hero tagline unified across README, docs, pyproject, and server
app.py: “AI memory that survives across sessions, teammates, and vendors.”
Known gaps¶
JWT sessions issued before a password reset remain valid until their TTL. Session revocation requires a token-denylist store that v0.6 does not ship.
Gif rendering for the README is deferred — VHS tape scripts and fixtures live in
demos/, butdemos/output/*.gifmust be regenerated locally via./demos/reset-db.sh && vhs demos/*.tape.
v0.5.3 — research item type¶
Migration: 0004_add_research_item_type
New system item type
researchseeded intoitem_types. Represents a cached external reference — typically a web page — that expires after a project-configurable TTL (.luplo/research.ttl_days, default 90).Partial CHECK constraint:
source_url IS NOT NULLfor rows withitem_type='research', so a research item cannot slip in without a URL.Added post-freeze (the v0.5 design on 2026-04-13 didn’t include it). Parity review against the dogfooding workflow showed research items were load-bearing and did not fit cleanly into
knowledge.
v0.5.2 — items as substrate¶
Migration: 0003_item_types_and_context
This is the substrate refactor. Earlier sprints had pencilled in
separate tasks and qa_checks tables. The v0.5.2 decision was to
promote items into a general-purpose row and represent tasks /
QA checks as item types with strictly-validated context JSONB.
Specifically:
New
item_typesregistry table —(key, display_name, schema, owner)with JSON-schema-per-type validation.items.item_typebecomes an FK intoitem_types.items.context JSONBadded — free-form per-type, validated againstitem_types.schema.Seven system types seeded:
decision,knowledge,policy,document,task,qa_check, plus the earlier-addedresearch(slotted in by v0.5.3).GIN indexes on
context->'target_task_ids'andcontext->'target_item_ids'so QA checks can be looked up by target in either direction.B-tree index on
(work_unit_id, (context->>'sort_order')::int)for task ordering.taskandqa_checkschemas are strict (additionalProperties=false);decision,knowledge,policy,document,researchstay permissive (see P6 for the “strict by intent, not by owner” rule).
Decisions tied to this change: D1 (items as substrate), D2 (DB as source of truth for types), P6 (schema strictness policy), P7 (no partial UNIQUE for in-progress task), P9 (in-place QA revalidation), P10 (in-place task reorder).
v0.5.1 — auth redesign¶
Migration: 0002_auth_redesign
actors.idchanged fromTEXTtoUUID. All ten FK columns referencing actors were migrated in the same transaction so the DB never sees a mixed state.actors.emailpromoted to the primary human identifier.actors.password_hash TEXT NULLadded. OAuth-only users leave it NULL.lp loginlands the JWT in the OS keyring.Minimal password flow (argon2id, no reset). Reset is deferred — adding it without compromising the “no web UI” stance is v0.6 material.
Reordering note: this sprint deliberately preceded tasks/qa because
every domain table FKs into actors — doing auth after would have
meant re-migrating them all.
v0.5 — initial 12-table schema¶
Migration: 0001_init_schema
Schema frozen on 2026-04-13 after a full-day design session. Twelve tables across three concerns:
Core (6):
projects,actors,systems,items,links,work_units.Sync (3):
items_history,audit_log,sync_jobs.Glossary (3):
glossary_groups,glossary_terms,glossary_rejections.
Design goals covered:
Items carry supersede chains + soft delete + semantic-impact history.
Work units replace sessions; they span multiple client sessions and record A→B handoffs.
Glossary is strict-first with a curation queue.
Vector search (pgvector) is optional and only reranks tsquery candidates.
Worker uses PG LISTEN/NOTIFY — no external broker.
Versioning note¶
Pre-1.0, luplo’s “versions” are the migration numbers. The PyPI
0.0.1 tag is a placeholder — real releases will track v0.5 / v0.6
milestones with a proper semantic version once the core is
dog-food-stable end-to-end.