Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions brain-bar/Sources/BrainBar/BrainDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,8 @@ final class BrainDatabase: @unchecked Sendable {
tag: String? = nil,
importanceMin: Double? = nil,
subscriberID: String? = nil,
unreadOnly: Bool = false
unreadOnly: Bool = false,
includeAudit: Bool = false
) throws -> [[String: Any]] {
guard db != nil else { throw DBError.notOpen }
let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
Expand All @@ -551,7 +552,8 @@ final class BrainDatabase: @unchecked Sendable {
project: project,
source: source,
tag: tag,
importanceMin: importanceMin
importanceMin: importanceMin,
includeAudit: includeAudit
) {
return exact
}
Expand All @@ -572,7 +574,8 @@ final class BrainDatabase: @unchecked Sendable {
importanceMin: importanceMin,
subscribedTags: subscribedTags,
ackFloor: ackFloor,
unreadOnly: unreadOnly
unreadOnly: unreadOnly,
includeAudit: includeAudit
)
maxRowID = max(maxRowID, searchResult.maxRowID)
appendDeduped(searchResult.rows, to: &results, seenChunkIDs: &seenChunkIDs, limit: limit)
Expand All @@ -593,7 +596,8 @@ final class BrainDatabase: @unchecked Sendable {
importanceMin: importanceMin,
subscribedTags: subscribedTags,
ackFloor: ackFloor,
unreadOnly: unreadOnly
unreadOnly: unreadOnly,
includeAudit: includeAudit
)
maxRowID = max(maxRowID, searchResult.maxRowID)
appendDeduped(searchResult.rows, to: &results, seenChunkIDs: &seenChunkIDs, limit: limit)
Expand All @@ -618,7 +622,8 @@ final class BrainDatabase: @unchecked Sendable {
importanceMin: Double?,
subscribedTags: [String],
ackFloor: Int64,
unreadOnly: Bool
unreadOnly: Bool,
includeAudit: Bool
) throws -> (rows: [[String: Any]], maxRowID: Int64) {
guard let db else { throw DBError.notOpen }
let allowedTables = ["chunks_fts", "chunks_fts_trigram"]
Expand All @@ -636,6 +641,7 @@ final class BrainDatabase: @unchecked Sendable {
let tagTerms = Array(repeating: "c.tags LIKE ?", count: subscribedTags.count).joined(separator: " OR ")
conditions.append("(\(tagTerms))")
}
if !includeAudit { conditions.append(Self.auditRecursionTagExclusionSQL(alias: "c")) }
if importanceMin != nil { conditions.append("c.importance >= ?") }
if unreadOnly { conditions.append("c.rowid > ?") }

Expand Down Expand Up @@ -907,7 +913,8 @@ final class BrainDatabase: @unchecked Sendable {
tag: String? = nil,
importanceMin: Double? = nil,
subscriberID: String? = nil,
unreadOnly: Bool = false
unreadOnly: Bool = false,
includeAudit: Bool = false
) throws -> [SearchQueryCandidate] {
guard let db else { throw DBError.notOpen }
let sanitized = sanitizeFTS5Query(query)
Expand All @@ -930,6 +937,7 @@ final class BrainDatabase: @unchecked Sendable {
let tagTerms = Array(repeating: "c.tags LIKE ?", count: subscribedTags.count).joined(separator: " OR ")
conditions.append("(\(tagTerms))")
}
if !includeAudit { conditions.append(Self.auditRecursionTagExclusionSQL(alias: "c")) }
if importanceMin != nil { conditions.append("c.importance >= ?") }
if unreadOnly { conditions.append("c.rowid > ?") }

Expand Down Expand Up @@ -1487,6 +1495,20 @@ final class BrainDatabase: @unchecked Sendable {
"""
}

private static func auditRecursionTagExclusionSQL(alias: String) -> String {
let tagsJSON = "CASE WHEN json_valid(\(alias).tags) THEN \(alias).tags ELSE '[]' END"
let tagValue = "LOWER(CAST(audit_tags.value AS TEXT))"
return """
NOT EXISTS (
SELECT 1
FROM json_each(\(tagsJSON)) audit_tags
WHERE \(tagValue) LIKE '%audit%'
OR \(tagValue) = 'agent=auditor'
OR \(tagValue) GLOB 'r0[0-9]'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add r0x shorthand to Swift audit exclusion

The default BrainBar filter claims to exclude r02/r0x audit recursion tags, but this SQL only matches GLOB 'r0[0-9]', so chunks tagged exactly r0x still leak into default brain_search results when include_audit is false. This creates an inconsistent contract versus the Python path (which does filter r0x) and allows the audit-recursion contamination this change is meant to prevent.

Useful? React with 👍 / 👎.

)
"""
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swift audit filter missing r0x tag exclusion

Medium Severity

The Swift auditRecursionTagExclusionSQL only checks LIKE '%audit%', = 'agent=auditor', and GLOB 'r0[0-9]'. It does not exclude chunks tagged r0x because 'x' is not a digit and "r0x" doesn't contain "audit". The Python counterpart explicitly includes = 'r0x' in AUDIT_RECURSION_TAG_PATTERNS. Chunks tagged solely with r0x will leak through in Swift but be correctly filtered in Python.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4a8c8ee. Configure here.


private func databaseSizeBytes() -> Int64 {
let candidates = [path, "\(path)-wal", "\(path)-shm"]
return candidates.reduce(into: Int64(0)) { total, candidate in
Expand Down Expand Up @@ -1813,7 +1835,8 @@ final class BrainDatabase: @unchecked Sendable {
project: String?,
source: String?,
tag: String?,
importanceMin: Double?
importanceMin: Double?,
includeAudit: Bool
) throws -> [[String: Any]]? {
guard let db else { throw DBError.notOpen }
guard limit > 0, !query.contains(where: { $0.isWhitespace }), query.contains("-") else {
Expand All @@ -1825,6 +1848,7 @@ final class BrainDatabase: @unchecked Sendable {
if project != nil { conditions.append("c.project = ?") }
if sourceFilter != nil { conditions.append("c.source = ?") }
if tag != nil { conditions.append("c.tags LIKE ?") }
if !includeAudit { conditions.append(Self.auditRecursionTagExclusionSQL(alias: "c")) }
if importanceMin != nil { conditions.append("c.importance >= ?") }

let sql = """
Expand Down
7 changes: 5 additions & 2 deletions brain-bar/Sources/BrainBar/MCPRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ final class MCPRouter: @unchecked Sendable {
let tag = args["tag"] as? String
let subscriberID = (args["agent_id"] as? String) ?? (args["subscriber_id"] as? String)
let unreadOnly = args["unread_only"] as? Bool ?? false
let includeAudit = args["include_audit"] as? Bool ?? false
let sourceCountsAsFilter: Bool
if let source {
let trimmed = source.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down Expand Up @@ -264,7 +265,8 @@ final class MCPRouter: @unchecked Sendable {
tag: tag,
importanceMin: importanceMin,
subscriberID: subscriberID,
unreadOnly: unreadOnly
unreadOnly: unreadOnly,
includeAudit: includeAudit
)
let typedResults = results.map(SearchResult.init(payload:))
let textSection = TextFormatter.formatSearchResults(query: query, results: typedResults, total: typedResults.count)
Expand Down Expand Up @@ -820,7 +822,7 @@ final class MCPRouter: @unchecked Sendable {
nonisolated(unsafe) static let toolDefinitions: [[String: Any]] = [
[
"name": "brain_search",
"description": "Search through past conversations and learnings. Hybrid semantic + keyword search.",
"description": "Search through past conversations and learnings. Hybrid semantic + keyword search. Audit/eval chunks tagged audit, r02/r0x, audit-pollution-source, or agent=auditor are excluded by default; set include_audit=true only when explicitly looking up audit history.",
"annotations": MCPRouter.readOnlyAnnotations,
"inputSchema": MCPRouter.limitedInputSchema([
"type": "object",
Expand All @@ -833,6 +835,7 @@ final class MCPRouter: @unchecked Sendable {
"importance_min": ["type": "number", "description": "Minimum importance score (1-10)"],
"agent_id": ["type": "string", "description": "Optional stable agent id for unread filtering"],
"unread_only": ["type": "boolean", "description": "Return only chunks not yet acknowledged by agent_id"],
"include_audit": ["type": "boolean", "description": "Opt in to audit/eval memories. Defaults false to prevent audit-recursion pollution."],
"detail": ["type": "string", "enum": ["compact", "full"], "description": "Result detail level"],
] as [String: Any],
"required": ["query"]
Expand Down
88 changes: 88 additions & 0 deletions brain-bar/Tests/BrainBarTests/MCPRouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,94 @@ final class MCPRouterTests: XCTestCase {
XCTAssertEqual(text.components(separatedBy: "Sagit meeting notes").count - 1, 1, "Only one matching source should be returned")
}

func testBrainSearchExcludesAuditRecursionByDefaultAndAllowsOptIn() throws {
let tempDB = NSTemporaryDirectory() + "brainbar-audit-filter-\(UUID().uuidString).db"
defer { try? FileManager.default.removeItem(atPath: tempDB) }
let db = BrainDatabase(path: tempDB)
defer { db.close() }

try db.insertChunk(
id: "audit-recursion-source",
content: "why restart BrainBar audit recursion contamination exact match",
sessionId: "s1",
project: "brainlayer",
contentType: "assistant_text",
importance: 8,
tags: "[\"r02\", \"audit\"]"
)
try db.insertChunk(
id: "ordinary-brainbar-memory",
content: "why restart BrainBar because launchd replaced the old degraded binary",
sessionId: "s2",
project: "brainlayer",
contentType: "assistant_text",
importance: 8,
tags: "[\"brainbar\", \"reliability\"]"
)

let router = MCPRouter()
router.setDatabase(db)
let defaultResponse = router.handle([
"jsonrpc": "2.0",
"id": 160,
"method": "tools/call",
"params": [
"name": "brain_search",
"arguments": ["query": "why restart BrainBar", "num_results": 3] as [String: Any]
] as [String: Any]
])
let defaultText = ((defaultResponse["result"] as? [String: Any])?["content"] as? [[String: Any]])?.first?["text"] as? String ?? ""

XCTAssertTrue(defaultText.contains("ordinary-bra"), defaultText)
XCTAssertFalse(defaultText.contains("audit-recurs"), defaultText)

let optInResponse = router.handle([
"jsonrpc": "2.0",
"id": 161,
"method": "tools/call",
"params": [
"name": "brain_search",
"arguments": ["query": "why restart BrainBar", "num_results": 3, "include_audit": true] as [String: Any]
] as [String: Any]
])
let optInText = ((optInResponse["result"] as? [String: Any])?["content"] as? [[String: Any]])?.first?["text"] as? String ?? ""

XCTAssertTrue(optInText.contains("audit-recurs"), optInText)
XCTAssertTrue(optInText.contains("ordinary-bra"), optInText)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func testBrainSearchDoesNotTreatR0xSubstringTagsAsAudit() throws {
let tempDB = NSTemporaryDirectory() + "brainbar-audit-substring-\(UUID().uuidString).db"
defer { try? FileManager.default.removeItem(atPath: tempDB) }
let db = BrainDatabase(path: tempDB)
defer { db.close() }

try db.insertChunk(
id: "ordinary-mirror07-memory",
content: "mirror07 normal operational memory should remain searchable",
sessionId: "s1",
project: "brainlayer",
contentType: "assistant_text",
importance: 8,
tags: "[\"mirror07\", \"reliability\"]"
)

let router = MCPRouter()
router.setDatabase(db)
let response = router.handle([
"jsonrpc": "2.0",
"id": 162,
"method": "tools/call",
"params": [
"name": "brain_search",
"arguments": ["query": "mirror07 normal operational memory", "num_results": 3] as [String: Any]
] as [String: Any]
])
let text = ((response["result"] as? [String: Any])?["content"] as? [[String: Any]])?.first?["text"] as? String ?? ""

XCTAssertTrue(text.contains("ordinary-mir"), text)
}

func testBrainSearchSourceAllKeepsKGAugmentation() throws {
let tempDB = NSTemporaryDirectory() + "brainbar-source-all-\(UUID().uuidString).db"
defer { try? FileManager.default.removeItem(atPath: tempDB) }
Expand Down
5 changes: 5 additions & 0 deletions src/brainlayer/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def think(
embed_fn: Any,
project: str | None = None,
max_results: int = 10,
include_audit: bool = False,
) -> ThinkResult:
"""Given current task context, retrieve relevant past knowledge.

Expand Down Expand Up @@ -206,6 +207,7 @@ def think(
n_results=max_results,
project_filter=project,
importance_min=3.0, # Skip low-importance noise
include_audit=include_audit,
)

if not results["documents"][0]:
Expand Down Expand Up @@ -239,6 +241,7 @@ def recall(
topic: str | None = None,
project: str | None = None,
max_results: int = 10,
include_audit: bool = False,
) -> RecallResult:
"""Proactive smart retrieval based on file or topic.

Expand Down Expand Up @@ -278,6 +281,7 @@ def recall(
query_text=fname,
n_results=max_results,
project_filter=project,
include_audit=include_audit,
)
for doc, meta in zip(search_results["documents"][0], search_results["metadatas"][0]):
result.related_chunks.append(
Expand All @@ -299,6 +303,7 @@ def recall(
query_text=topic,
n_results=max_results,
project_filter=project,
include_audit=include_audit,
)
for doc, meta in zip(search_results["documents"][0], search_results["metadatas"][0]):
result.related_chunks.append(
Expand Down
16 changes: 14 additions & 2 deletions src/brainlayer/mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ async def list_tools() -> list[Tool]:
Tool(
name="brain_search",
title="Search Knowledge Base",
description="""Search BrainLayer's persistent memory for past decisions, project history, debugging notes, preferences, and other stored knowledge. Use when: the user asks what was decided before, how something was implemented, what happened to a file, or what you are working on. Don't use when: you need current session context or stats (use brain_recall), a named entity graph lookup (use brain_entity), or to save new information (use brain_store). query should be a natural-language lookup phrase; file_path switches to file-history routing, chunk_id expands a known result, and project narrows scope. num_results defaults to 5 and detail defaults to 'compact'; add date, tag, intent, or source filters only when they materially narrow the search. Returns ranked matches with scores, metadata, and compact snippets or full content; after finding a promising chunk, call brain_search with chunk_id or use brain_recall for session-level context.""",
description="""Search BrainLayer's persistent memory for decisions, project history, debugging notes, preferences, and stored knowledge. Use when: the user asks what was decided before, how something was implemented, what happened to a file, or what you are working on. Don't use when: you need current session context or stats (use brain_recall), a named entity graph lookup (use brain_entity), or to save new information (use brain_store). query is natural language; file_path switches to file-history routing, chunk_id expands a known result, and project narrows scope. num_results defaults to 5 and detail defaults to 'compact'; add date, tag, intent, or source filters only when useful. Audit/eval chunks tagged audit, r02/r0x, audit-pollution-source, or agent=auditor are excluded by default; set include_audit=true only for audit history. Returns ranked matches with scores, metadata, snippets, or full content.""",
annotations=_READ_ONLY,
inputSchema=_bounded_input_schema(
{
Expand Down Expand Up @@ -505,6 +505,11 @@ async def list_tools() -> list[Tool]:
"type": "string",
"description": "Filter by correction type tag (e.g. 'correction:preference', 'correction:factual', 'correction:naming'). Matches chunks tagged with the given correction category.",
},
"include_audit": {
"type": "boolean",
"default": False,
"description": "Opt in to audit/eval memories tagged audit, r02/r0x, audit-pollution-source, or agent=auditor. Defaults false to prevent audit-recursion pollution.",
},
"detail": {
"type": "string",
"enum": ["compact", "full"],
Expand Down Expand Up @@ -652,7 +657,7 @@ async def list_tools() -> list[Tool]:
Tool(
name="brain_recall",
title="Recall / Search / Entity Lookup",
description="""Get working context, recent sessions, plan/session links, per-session operations, summaries, stats, or routed search from one entry point. Use when: you need 'what am I working on', recent session history, plan linkage, operation groups for a session, or knowledge-base health stats. Don't use when: you already know you want topical memory search (use brain_search), a direct entity graph lookup (use brain_entity), or to store or digest new content (use brain_store or brain_digest). mode can be explicit or auto-detected from query; session_id is required for operations and summary, plan_name targets plan mode, and hours, days, and limit control context windows. In search mode, file_path, chunk_id, content filters, num_results, and detail='compact'|'full' behave like brain_search. Returns structured context, search results, or stats depending on mode; use brain_search after broad routing when you need tighter topical retrieval.""",
description="""Get working context, recent sessions, plan/session links, per-session operations, summaries, stats, or routed search from one entry point. Use when: you need 'what am I working on', recent session history, plan linkage, operation groups for a session, or knowledge-base health stats. Don't use when: you already know you want topical memory search (use brain_search), a direct entity graph lookup (use brain_entity), or to store or digest new content (use brain_store or brain_digest). mode can be explicit or auto-detected from query; session_id is required for operations and summary, plan_name targets plan mode, and hours, days, and limit control context windows. In search mode, file_path, chunk_id, content filters, num_results, include_audit, and detail='compact'|'full' behave like brain_search. Returns structured context, search results, or stats depending on mode; use brain_search after broad routing when you need tighter topical retrieval.""",
annotations=_READ_ONLY,
inputSchema=_bounded_input_schema(
{
Expand Down Expand Up @@ -792,6 +797,11 @@ async def list_tools() -> list[Tool]:
"default": "compact",
"description": "Result detail level (mode=search). 'compact': snippet + metadata. 'full': complete content.",
},
"include_audit": {
"type": "boolean",
"default": False,
"description": "Opt in to audit/eval memories in search mode. Defaults false to prevent audit-recursion pollution.",
},
},
}
),
Expand Down Expand Up @@ -1218,6 +1228,7 @@ async def call_tool(name: str, arguments: dict[str, Any]):
detail=arguments.get("detail", "compact"),
source_filter=resolved_source_filter,
correction_category=arguments.get("correction_category"),
include_audit=arguments.get("include_audit", False),
)
)

Expand Down Expand Up @@ -1297,6 +1308,7 @@ async def call_tool(name: str, arguments: dict[str, Any]):
max_results=arguments.get("max_results", 10),
detail=arguments.get("detail", "compact"),
entity_type=arguments.get("entity_type"),
include_audit=arguments.get("include_audit", False),
)
)

Expand Down
Loading
Loading