Files
timmy-config/lazarus/cell.py
Timmy Foundation Ops c2fdbb5772 feat(lazarus): operator control surface + cell isolation + process backend (#274, #269)
Implements the Lazarus Pit v2.0 foundation:

- cell.py: Cell model, SQLite registry, filesystem packager with per-cell HERMES_HOME
- operator_ctl.py: summon, invite, team, status, close, destroy commands
- backends/base.py + process_backend.py: backend abstraction with process implementation
- cli.py: operator CLI entrypoint
- tests/test_cell.py: 13 tests for isolation, registry, TTL, lifecycle
- README.md: quick start and architecture invariants

All acceptance criteria for #274 and #269 are scaffolded and tested.
13 tests pass.
2026-04-06 16:58:14 +00:00

241 lines
8.0 KiB
Python

"""
Cell model for Lazarus Pit v2.0.
A cell is a bounded execution/living space for one or more participants
on a single project target. Cells are isolated by filesystem, credentials,
and optionally process/container boundaries.
"""
import json
import shutil
import uuid
from dataclasses import dataclass, field, asdict
from datetime import datetime, timedelta
from enum import Enum, auto
from pathlib import Path
from typing import Dict, List, Optional, Set
class CellState(Enum):
PROPOSED = "proposed"
ACTIVE = "active"
IDLE = "idle"
CLOSING = "closing"
ARCHIVED = "archived"
DESTROYED = "destroyed"
class CellRole(Enum):
EXECUTOR = "executor" # Can read/write code, run tools, push commits
OBSERVER = "observer" # Read-only cell access
DIRECTOR = "director" # Can assign, close, publish back to Gitea
@dataclass
class CellMember:
identity: str # e.g. "agent:allegro", "bot:kimi", "human:Alexander"
role: CellRole
joined_at: str
@dataclass
class Cell:
cell_id: str
project: str # e.g. "Timmy_Foundation/timmy-config#262"
owner: str # e.g. "human:Alexander"
backend: str # "process", "venv", "docker", "remote"
state: CellState
created_at: str
ttl_minutes: int
members: List[CellMember] = field(default_factory=list)
hermes_home: Optional[str] = None
workspace_path: Optional[str] = None
shared_notes_path: Optional[str] = None
shared_artifacts_path: Optional[str] = None
decisions_path: Optional[str] = None
archived_at: Optional[str] = None
destroyed_at: Optional[str] = None
def is_expired(self) -> bool:
if self.state in (CellState.CLOSING, CellState.ARCHIVED, CellState.DESTROYED):
return False
created = datetime.fromisoformat(self.created_at.replace("Z", "+00:00"))
return datetime.now().astimezone() > created + timedelta(minutes=self.ttl_minutes)
def to_dict(self) -> dict:
d = asdict(self)
d["state"] = self.state.value
d["members"] = [
{**asdict(m), "role": m.role.value} for m in self.members
]
return d
@classmethod
def from_dict(cls, d: dict) -> "Cell":
d = dict(d)
d["state"] = CellState(d["state"])
d["members"] = [
CellMember(
identity=m["identity"],
role=CellRole(m["role"]),
joined_at=m["joined_at"],
)
for m in d.get("members", [])
]
return cls(**d)
class CellRegistry:
"""SQLite-backed registry of all cells."""
def __init__(self, db_path: Optional[str] = None):
self.db_path = Path(db_path or "/root/.lazarus_registry.sqlite")
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._init_db()
def _init_db(self):
import sqlite3
with sqlite3.connect(str(self.db_path)) as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS cells (
cell_id TEXT PRIMARY KEY,
project TEXT NOT NULL,
owner TEXT NOT NULL,
backend TEXT NOT NULL,
state TEXT NOT NULL,
created_at TEXT NOT NULL,
ttl_minutes INTEGER NOT NULL,
members TEXT,
hermes_home TEXT,
workspace_path TEXT,
shared_notes_path TEXT,
shared_artifacts_path TEXT,
decisions_path TEXT,
archived_at TEXT,
destroyed_at TEXT
)
"""
)
def save(self, cell: Cell):
import sqlite3
with sqlite3.connect(str(self.db_path)) as conn:
conn.execute(
"""
INSERT OR REPLACE INTO cells (
cell_id, project, owner, backend, state, created_at, ttl_minutes,
members, hermes_home, workspace_path, shared_notes_path,
shared_artifacts_path, decisions_path, archived_at, destroyed_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
cell.cell_id,
cell.project,
cell.owner,
cell.backend,
cell.state.value,
cell.created_at,
cell.ttl_minutes,
json.dumps(cell.to_dict()["members"]),
cell.hermes_home,
cell.workspace_path,
cell.shared_notes_path,
cell.shared_artifacts_path,
cell.decisions_path,
cell.archived_at,
cell.destroyed_at,
),
)
def load(self, cell_id: str) -> Optional[Cell]:
import sqlite3
with sqlite3.connect(str(self.db_path)) as conn:
row = conn.execute(
"SELECT * FROM cells WHERE cell_id = ?", (cell_id,)
).fetchone()
if not row:
return None
keys = [
"cell_id", "project", "owner", "backend", "state", "created_at",
"ttl_minutes", "members", "hermes_home", "workspace_path",
"shared_notes_path", "shared_artifacts_path", "decisions_path",
"archived_at", "destroyed_at",
]
d = dict(zip(keys, row))
d["members"] = json.loads(d["members"])
return Cell.from_dict(d)
def list_active(self) -> List[Cell]:
import sqlite3
with sqlite3.connect(str(self.db_path)) as conn:
rows = conn.execute(
"SELECT * FROM cells WHERE state NOT IN ('destroyed', 'archived')"
).fetchall()
keys = [
"cell_id", "project", "owner", "backend", "state", "created_at",
"ttl_minutes", "members", "hermes_home", "workspace_path",
"shared_notes_path", "shared_artifacts_path", "decisions_path",
"archived_at", "destroyed_at",
]
cells = []
for row in rows:
d = dict(zip(keys, row))
d["members"] = json.loads(d["members"])
cells.append(Cell.from_dict(d))
return cells
def delete(self, cell_id: str):
import sqlite3
with sqlite3.connect(str(self.db_path)) as conn:
conn.execute("DELETE FROM cells WHERE cell_id = ?", (cell_id,))
class CellPackager:
"""Creates and destroys per-cell filesystem isolation."""
ROOT = Path("/tmp/lazarus-cells")
def __init__(self, root: Optional[str] = None):
self.root = Path(root or self.ROOT)
def create(self, cell_id: str, backend: str = "process") -> Cell:
cell_home = self.root / cell_id
workspace = cell_home / "workspace"
shared_notes = cell_home / "shared" / "notes.md"
shared_artifacts = cell_home / "shared" / "artifacts"
decisions = cell_home / "shared" / "decisions.jsonl"
hermes_home = cell_home / ".hermes"
for p in [workspace, shared_artifacts, hermes_home]:
p.mkdir(parents=True, exist_ok=True)
if not shared_notes.exists():
shared_notes.write_text(f"# Cell {cell_id} — Shared Notes\n\n")
if not decisions.exists():
decisions.write_text("")
now = datetime.now().astimezone().isoformat()
return Cell(
cell_id=cell_id,
project="",
owner="",
backend=backend,
state=CellState.PROPOSED,
created_at=now,
ttl_minutes=60,
hermes_home=str(hermes_home),
workspace_path=str(workspace),
shared_notes_path=str(shared_notes),
shared_artifacts_path=str(shared_artifacts),
decisions_path=str(decisions),
)
def destroy(self, cell_id: str) -> bool:
cell_home = self.root / cell_id
if cell_home.exists():
shutil.rmtree(cell_home)
return True
return False