WIP: feat(curry): add versioned ledger tools and direct commands #10
@@ -126,6 +126,9 @@ React 19 + Vite. No routing library — `App.jsx` manages page state in a single
|
|||||||
### Persistent Storage
|
### Persistent Storage
|
||||||
Most data lands in `synapse/memory/memory.db` (SQLite, WAL mode). Tables: memory facts, conversations, messages, app settings. `synapse/memory/store.py` (`PersistentMemoryStore`) owns the schema and all queries. Playbooks are the exception — they live as YAML files in `data/playbooks/` (see Playbook System). `nexus_config.py` defines all paths; it also ensures all required directories exist on import.
|
Most data lands in `synapse/memory/memory.db` (SQLite, WAL mode). Tables: memory facts, conversations, messages, app settings. `synapse/memory/store.py` (`PersistentMemoryStore`) owns the schema and all queries. Playbooks are the exception — they live as YAML files in `data/playbooks/` (see Playbook System). `nexus_config.py` defines all paths; it also ensures all required directories exist on import.
|
||||||
|
|
||||||
|
### Curry (`synapse/curry_core.py` + `synapse/curry_store.py`)
|
||||||
|
`curry_core.py` is vendored, unmodified-except-for-one-fix, from [Athena-Pro/Curry](https://github.com/Athena-Pro/Curry) — an immutable, versioned fact store (constants, functions, model registrations, inference provenance) backed by its own SQLite file (`CURRY_DB` in `nexus_config.py`, separate from `memory.db`). `curry_store.py` opens it into a module-level singleton (`curry_db`) at import time — the same pattern as `memory.store.store` / `playbooks.store.playbook_store` — so it's preloaded and callable (`curry_db.declare_constant(...)`, `curry_db.call_function(...)`, etc.) from anywhere in the backend without extra setup. It ships inside the wheel (`bin/check.sh`'s packaging gate asserts this) and has no external dependencies of its own. Nothing currently wires chat/model-authored content into it — it's available, not yet exposed as an action tool. Re-sync `curry_core.py` from upstream by hand, not by script; see the file's own docstring for what changed and why.
|
||||||
|
|
||||||
### Logs & Runtime State
|
### Logs & Runtime State
|
||||||
- `runtime/backend.log`, `runtime/frontend.log`, `runtime/memory.log` — service stdout
|
- `runtime/backend.log`, `runtime/frontend.log`, `runtime/memory.log` — service stdout
|
||||||
- `runtime/logs/ollama.log`, `runtime/logs/chat.log`
|
- `runtime/logs/ollama.log`, `runtime/logs/chat.log`
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ if not any(n.startswith("synapse/_resources/web/") for n in names):
|
|||||||
sys.exit("wheel is missing the compiled web UI (cd interface/web && npm run build)")
|
sys.exit("wheel is missing the compiled web UI (cd interface/web && npm run build)")
|
||||||
if not any(n.startswith("synapse/_resources/playbooks/") for n in names):
|
if not any(n.startswith("synapse/_resources/playbooks/") for n in names):
|
||||||
sys.exit("wheel is missing the seed playbooks")
|
sys.exit("wheel is missing the seed playbooks")
|
||||||
|
if "synapse/curry_core.py" not in names or "synapse/curry_store.py" not in names:
|
||||||
|
sys.exit("wheel is missing vendored Curry (synapse/curry_core.py / curry_store.py)")
|
||||||
print(f"wheel OK: {len(names)} files")
|
print(f"wheel OK: {len(names)} files")
|
||||||
PY
|
PY
|
||||||
else
|
else
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
"""NexusOS's own Curry instance: preloaded at import time, ready to be called.
|
||||||
|
|
||||||
|
Curry (curry_core.py, vendored alongside this file) is an immutable, versioned
|
||||||
|
fact store - constants, functions, model registrations, and inference
|
||||||
|
provenance, backed by SQLite. Nothing in NexusOS wires chat/model-authored
|
||||||
|
content into it yet; this module only makes it available - `from
|
||||||
|
synapse.curry_store import curry_db` and call `declare_constant`,
|
||||||
|
`get_constant_latest`, `declare_function`, `call_function`, etc. directly, the
|
||||||
|
same way `synapse.memory.store.store` and `synapse.playbooks.store.playbook_store`
|
||||||
|
are used elsewhere in this codebase.
|
||||||
|
|
||||||
|
Kept as a separate database file (CURRY_DB) from the memory/conversation store
|
||||||
|
on purpose: Curry's schema and lifecycle are independent of the memory store's.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .curry_core import Curry
|
||||||
|
from .nexus_config import CURRY_DB
|
||||||
|
|
||||||
|
curry_db = Curry(str(CURRY_DB))
|
||||||
|
|
||||||
|
__all__ = ["curry_db"]
|
||||||
@@ -182,6 +182,7 @@ async def _generate_conversation_title(first_message: str, model: str) -> Option
|
|||||||
|
|
||||||
from .memory.store import store, MemoryItem
|
from .memory.store import store, MemoryItem
|
||||||
from .playbooks.store import playbook_store, PlaybookItem
|
from .playbooks.store import playbook_store, PlaybookItem
|
||||||
|
from .curry_store import curry_db # noqa: F401 - import triggers Curry's own preload at startup
|
||||||
from .search import needs_web_search, web_search
|
from .search import needs_web_search, web_search
|
||||||
|
|
||||||
MEMORY_SERVICE = settings.memory_url
|
MEMORY_SERVICE = settings.memory_url
|
||||||
|
|||||||
@@ -160,6 +160,11 @@ SEED_PLAYBOOK_DIR = (
|
|||||||
|
|
||||||
# --- DATABASE / STORAGE FILES (match your repo) ---
|
# --- DATABASE / STORAGE FILES (match your repo) ---
|
||||||
MEMORY_DB = _configured_path("memory_db", "NEXUS_MEMORY_DB", MEMORY_DIR / "memory.db")
|
MEMORY_DB = _configured_path("memory_db", "NEXUS_MEMORY_DB", MEMORY_DIR / "memory.db")
|
||||||
|
# Vendored Curry (synapse/curry_core.py) database: immutable versioned
|
||||||
|
# constants/functions/models + inference provenance. Separate file from
|
||||||
|
# MEMORY_DB on purpose - Curry's schema and lifecycle are independent of the
|
||||||
|
# memory/conversation store.
|
||||||
|
CURRY_DB = _configured_path("curry_db", "NEXUS_CURRY_DB", DATA_DIR / "curry.db")
|
||||||
|
|
||||||
# --- LOG FILES ---
|
# --- LOG FILES ---
|
||||||
BACKEND_LOG = RUNTIME_DIR / "backend.log"
|
BACKEND_LOG = RUNTIME_DIR / "backend.log"
|
||||||
@@ -180,6 +185,7 @@ _REQUIRED_DIRS = (
|
|||||||
UPLOADS_DIR,
|
UPLOADS_DIR,
|
||||||
EXPORTS_DIR,
|
EXPORTS_DIR,
|
||||||
MEMORY_DB.parent,
|
MEMORY_DB.parent,
|
||||||
|
CURRY_DB.parent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -424,7 +430,7 @@ __all__ = ["Settings", "settings", "path", "VERSION",
|
|||||||
"read_user_config", "write_user_config", "init_state", "INITIALIZED_FILES",
|
"read_user_config", "write_user_config", "init_state", "INITIALIZED_FILES",
|
||||||
"DATA_DIR", "MODELS_DIR", "RUNTIME_DIR",
|
"DATA_DIR", "MODELS_DIR", "RUNTIME_DIR",
|
||||||
"MEMORY_DIR", "LOGS_DIR", "PLAYBOOK_DIR", "UPLOADS_DIR",
|
"MEMORY_DIR", "LOGS_DIR", "PLAYBOOK_DIR", "UPLOADS_DIR",
|
||||||
"EXPORTS_DIR", "MEMORY_DB", "WEB_DIST_DIR", "FRONTEND_SOURCE_DIR",
|
"EXPORTS_DIR", "MEMORY_DB", "CURRY_DB", "WEB_DIST_DIR", "FRONTEND_SOURCE_DIR",
|
||||||
"ASSETS_DIR", "SEED_PLAYBOOK_DIR",
|
"ASSETS_DIR", "SEED_PLAYBOOK_DIR",
|
||||||
"BACKEND_LOG", "OLLAMA_LOG", "CHAT_LOG",
|
"BACKEND_LOG", "OLLAMA_LOG", "CHAT_LOG",
|
||||||
"ALLOWED_HOSTS", "ALLOWED_ORIGINS",
|
"ALLOWED_HOSTS", "ALLOWED_ORIGINS",
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""synapse/curry_core.py (vendored) + synapse/curry_store.py (NexusOS's preload).
|
||||||
|
|
||||||
|
Two concerns: the vendor sync didn't silently drop the sandbox fix from
|
||||||
|
https://github.com/Athena-Pro/Curry/pull/4, and curry_store actually gives
|
||||||
|
NexusOS a live, callable instance without wiring it into any chat-facing tool.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from synapse.curry_core import Curry, TypeSignature
|
||||||
|
from synapse import curry_store
|
||||||
|
|
||||||
|
|
||||||
|
def test_curry_store_is_preloaded_and_callable():
|
||||||
|
# curry_store.curry_db is a module-level singleton constructed at import
|
||||||
|
# time (mirrors synapse.memory.store.store / synapse.playbooks.store.playbook_store)
|
||||||
|
# - by the time this test runs, it has already opened its database file.
|
||||||
|
assert isinstance(curry_store.curry_db, Curry)
|
||||||
|
curry_store.curry_db.declare_constant("t_preload_check", 1, 1, TypeSignature.INT32.value)
|
||||||
|
assert curry_store.curry_db.get_constant_latest("t_preload_check")["value"] == 1
|
||||||
|
curry_store.curry_db.retire_constant("t_preload_check", 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_curry_db_path_matches_nexus_config(tmp_path, monkeypatch):
|
||||||
|
from synapse import nexus_config
|
||||||
|
assert str(curry_store.curry_db.db_path) == str(nexus_config.CURRY_DB)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vendored_sandbox_fix_rejects_format_dunder_escape(tmp_path):
|
||||||
|
# Regression test for the vendored fix: a body that hides dunder-attribute
|
||||||
|
# traversal inside a str.format() field spec must still be rejected at
|
||||||
|
# declare time, not just the literal '.__class__' form. If a future
|
||||||
|
# re-vendor from upstream drops the fix, this is what catches it.
|
||||||
|
db = Curry(str(tmp_path / "sandbox_check.db"))
|
||||||
|
db.declare_function("helper", 1, "1")
|
||||||
|
|
||||||
|
exploit = "'{0.__globals__}'.format(helper)"
|
||||||
|
with pytest.raises(ValueError, match="format"):
|
||||||
|
db.declare_function("evil", 1, exploit, function_bindings={"helper": 1})
|
||||||
|
|
||||||
|
# the original, always-caught dunder-attribute form stays blocked too
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
db.declare_function("evil2", 1, "x.__class__", expected_args=["x"])
|
||||||
|
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_vendored_curry_basic_versioning_roundtrip(tmp_path):
|
||||||
|
db = Curry(str(tmp_path / "roundtrip.db"))
|
||||||
|
db.declare_constant("rate", 1, 0.1, TypeSignature.FLOAT64.value)
|
||||||
|
db.declare_function(
|
||||||
|
"apply_rate", 1, "amount * (1 + rate)",
|
||||||
|
constant_bindings={"rate": 1}, expected_args=["amount"],
|
||||||
|
)
|
||||||
|
assert db.call_function("apply_rate", 1, {"amount": 100}) == 110.00000000000001
|
||||||
|
db.close()
|
||||||
Reference in New Issue
Block a user