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
+15 -4
View File
@@ -285,12 +285,17 @@ async def chat_stream_endpoint(payload: Dict[str, Any]):
separator = "\n\n---\nRelevant past exchanges (use as background context only):\n\n"
system_prompt = (system_prompt + separator + memory_block) if system_prompt else memory_block
# Resolve the RAG scope: an existing conversation keeps its bound project;
# a brand-new one inherits the current workspace (active_project setting).
_conv_proj = store.conversation_project(conversation_id)
rag_scope = _conv_proj if _conv_proj is not None else app_settings.get("active_project", "")
# Retrieve relevant uploaded documents (RAG) and inject the top chunks.
doc_hits = await store.search_documents(
message, get_ollama_manager().embed,
limit=app_settings.get("rag_top_k", 3),
min_score=app_settings.get("rag_min_score", 0.6),
project_id=app_settings.get("active_project", "") or None,
project_id=rag_scope or None,
)
doc_titles: list = []
if doc_hits:
@@ -365,14 +370,20 @@ async def chat_stream_endpoint(payload: Dict[str, Any]):
metadata["images"] = images
# Tool-using playbook: advertise the active playbook's allowlisted tools.
# Action tools (web/fetch/write) are withheld unless the consent gate is on.
if _main_pb and getattr(_main_pb, "tools", None):
schemas = _tools.schemas_for(_main_pb.tools)
allow_actions = bool(app_settings.get("allow_action_tools", False))
schemas = _tools.schemas_for(_main_pb.tools, allow_actions)
if schemas:
metadata["tools"] = schemas
_synapse_trace(f" TOOLS : {', '.join(_main_pb.tools)}\n")
_granted = [t for t in _main_pb.tools if not _tools.is_action(t) or allow_actions]
_withheld = [t for t in _main_pb.tools if _tools.is_action(t) and not allow_actions]
_synapse_trace(f" TOOLS : {', '.join(_granted)}\n")
if _withheld:
_synapse_trace(f" WITHHELD: {', '.join(_withheld)} (action tools off)\n")
# Persist conversation and user message before streaming
store.create_conversation(conversation_id)
store.create_conversation(conversation_id, rag_scope or "")
store.add_message(conversation_id, "user", rendered_message)
async def event_stream() -> AsyncGenerator[str, None]: