feat: per-conversation project binding + action-tool consent gate

- Conversations bind to a project on creation; RAG scopes to the
  conversation's project, not the global setting.
- Action tools (web_search/fetch_url/remember) are withheld unless
  allow_action_tools is enabled (off by default). Settings toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-23 14:29:40 -05:00
co-authored by Claude Opus 4.8
parent ba6a4ac4e4
commit 492020b547
6 changed files with 91 additions and 10 deletions
+21 -3
View File
@@ -140,6 +140,11 @@ class PersistentMemoryStore:
cur.execute("ALTER TABLE conversations ADD COLUMN title TEXT")
except Exception:
pass
# Migrate: bind a conversation to a project ("" = unscoped).
try:
cur.execute("ALTER TABLE conversations ADD COLUMN project_id TEXT NOT NULL DEFAULT ''")
except Exception:
pass
cur.execute("""
CREATE TABLE IF NOT EXISTS messages (
@@ -364,20 +369,30 @@ class PersistentMemoryStore:
# -----------------------------
# Conversation API
# -----------------------------
def create_conversation(self, conversation_id: str) -> ConversationItem:
def create_conversation(self, conversation_id: str, project_id: str = "") -> ConversationItem:
now = time.time()
conn = self._connect()
try:
cur = conn.cursor()
cur.execute(
"INSERT OR IGNORE INTO conversations (id, created_at, updated_at) VALUES (?, ?, ?)",
(conversation_id, now, now)
"INSERT OR IGNORE INTO conversations (id, created_at, updated_at, project_id) VALUES (?, ?, ?, ?)",
(conversation_id, now, now, project_id or "")
)
conn.commit()
finally:
conn.close()
return ConversationItem(id=conversation_id, created_at=now, updated_at=now)
def conversation_project(self, conversation_id: str) -> Optional[str]:
"""The conversation's bound project ('' = unscoped), or None if it doesn't
exist yet — lets a new chat inherit the current workspace."""
conn = self._connect()
row = conn.execute(
"SELECT project_id FROM conversations WHERE id = ?", (conversation_id,)
).fetchone()
conn.close()
return None if row is None else (row["project_id"] or "")
def set_conversation_title(self, conversation_id: str, title: str):
conn = self._connect()
try:
@@ -983,6 +998,9 @@ class PersistentMemoryStore:
"rag_min_score": 0.6,
# Active project/workspace; "" = all documents (unscoped).
"active_project": "",
# Consent gate for tools that act (web_search/fetch_url/remember). Off by
# default: a playbook can list them, but they only run when this is on.
"allow_action_tools": False,
"system_prompt": "",
"timeout": 120,
# How long Ollama keeps the model resident in VRAM between messages.