mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Fix libsql prompt scope regressions (#1651)
This commit is contained in:
@@ -63,7 +63,12 @@ impl Agent {
|
||||
);
|
||||
|
||||
let system_prompt = if let Some(ws) = self.workspace() {
|
||||
match ws
|
||||
let scoped_workspace = if ws.user_id() == message.user_id {
|
||||
Arc::clone(ws)
|
||||
} else {
|
||||
Arc::new(ws.scoped_to_user(&message.user_id))
|
||||
};
|
||||
match scoped_workspace
|
||||
.system_prompt_for_context_tz(is_group_chat, user_tz)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -149,6 +149,7 @@ fn reject_if_injected(path: &str, content: &str) -> Result<(), WorkspaceError> {
|
||||
///
|
||||
/// Allows Workspace to work with either a PostgreSQL `Repository` (the original
|
||||
/// path) or any `Database` trait implementation (e.g. libSQL backend).
|
||||
#[derive(Clone)]
|
||||
enum WorkspaceStorage {
|
||||
/// PostgreSQL-backed repository (uses connection pool directly).
|
||||
#[cfg(feature = "postgres")]
|
||||
@@ -576,6 +577,60 @@ impl Workspace {
|
||||
self
|
||||
}
|
||||
|
||||
/// Clone the workspace configuration for a different primary user scope.
|
||||
///
|
||||
/// This preserves search config, embeddings, shared read scopes, memory
|
||||
/// layers, and privacy classifier while switching the primary read/write
|
||||
/// scope to `user_id`.
|
||||
pub fn scoped_to_user(&self, user_id: impl Into<String>) -> Self {
|
||||
let user_id = user_id.into();
|
||||
|
||||
let mut memory_layers = self.memory_layers.clone();
|
||||
for layer in &mut memory_layers {
|
||||
if layer.sensitivity == crate::workspace::layer::LayerSensitivity::Private
|
||||
&& layer.scope == self.user_id
|
||||
{
|
||||
layer.scope = user_id.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let mut read_user_ids = vec![user_id.clone()];
|
||||
for scope in &self.read_user_ids {
|
||||
if scope != &self.user_id && !read_user_ids.contains(scope) {
|
||||
read_user_ids.push(scope.clone());
|
||||
}
|
||||
}
|
||||
for scope in crate::workspace::layer::MemoryLayer::read_scopes(&memory_layers) {
|
||||
if !read_user_ids.contains(&scope) {
|
||||
read_user_ids.push(scope);
|
||||
}
|
||||
}
|
||||
|
||||
let preserve_flags = user_id == self.user_id;
|
||||
Self {
|
||||
user_id,
|
||||
read_user_ids,
|
||||
agent_id: self.agent_id,
|
||||
storage: self.storage.clone(),
|
||||
embeddings: self.embeddings.clone(),
|
||||
bootstrap_pending: std::sync::atomic::AtomicBool::new(if preserve_flags {
|
||||
self.bootstrap_pending
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
} else {
|
||||
false
|
||||
}),
|
||||
bootstrap_completed: std::sync::atomic::AtomicBool::new(if preserve_flags {
|
||||
self.bootstrap_completed
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
} else {
|
||||
false
|
||||
}),
|
||||
search_defaults: self.search_defaults.clone(),
|
||||
memory_layers,
|
||||
privacy_classifier: self.privacy_classifier.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the user ID (primary scope for writes).
|
||||
pub fn user_id(&self) -> &str {
|
||||
&self.user_id
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::workspace::document::{MemoryChunk, MemoryDocument, WorkspaceEntry};
|
||||
use crate::workspace::search::{RankedResult, SearchConfig, SearchResult, fuse_results};
|
||||
|
||||
/// Database repository for workspace operations.
|
||||
#[derive(Clone)]
|
||||
pub struct Repository {
|
||||
pool: Pool,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ mod tests {
|
||||
|
||||
use crate::support::test_rig::TestRigBuilder;
|
||||
use crate::support::trace_llm::LlmTrace;
|
||||
use ironclaw::workspace::Workspace;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 1: write_chunk_search
|
||||
@@ -268,6 +269,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn identity_in_system_prompt() {
|
||||
const TEST_USER_ID: &str = "test-user";
|
||||
let trace = LlmTrace::from_file(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/llm_traces/workspace/identity_prompt.json"
|
||||
@@ -280,7 +282,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
// Seed an IDENTITY.md so the system prompt has real content to inject.
|
||||
let ws = rig.workspace().expect("workspace must be available");
|
||||
let ws = Workspace::new_with_db(TEST_USER_ID, rig.database().clone());
|
||||
ws.write(
|
||||
"IDENTITY.md",
|
||||
"I am TestBot, a helpful testing assistant created for E2E verification.",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Tests proving that multi-tenant system prompts are broken.
|
||||
//! Regression tests for multi-tenant system prompts.
|
||||
//!
|
||||
//! Bug: In multi-tenant mode, the agent loop uses `self.workspace()` which
|
||||
//! returns a single shared workspace (user_id="default"). Identity files
|
||||
//! (IDENTITY.md, SOUL.md, USER.md) seeded under per-user IDs ("alice",
|
||||
//! "bob") are invisible to this workspace, so the system prompt is
|
||||
//! empty/wrong.
|
||||
//! The agent must build the conversational system prompt from a workspace
|
||||
//! scoped to the incoming message's user, not from the shared owner-scope
|
||||
//! workspace created at startup. Otherwise per-user identity files
|
||||
//! (IDENTITY.md, SOUL.md, USER.md) become invisible and different users can
|
||||
//! see the same owner-scoped prompt.
|
||||
//!
|
||||
//! These tests:
|
||||
//! 1. Seed identity files for two users (alice, bob) in the database
|
||||
@@ -13,7 +13,7 @@
|
||||
//! correct user's identity
|
||||
//! 4. Verify user A's identity doesn't leak into user B's prompt
|
||||
//!
|
||||
//! All tests are expected to FAIL until the bug is fixed.
|
||||
//! These tests ensure each user's identity is isolated correctly.
|
||||
|
||||
#[cfg(feature = "libsql")]
|
||||
mod support;
|
||||
|
||||
Reference in New Issue
Block a user