Files
viettranx c7b2df9e34 feat(vault): chat_id isolation for isolated teams
- Add vault_documents.chat_id + composite index (migration 000056)
- Filter vault_search by chat_id when team.workspace_scope=isolated
- Stamp chat_id on AfterWrite/AfterWriteMedia for isolated teams
- Deny cross-chat vault_read in isolated teams (M2 fix)
- RunContext.TeamIsolated flag resolved once per run
- Fallback WorkspaceChatID → ChatID in loop_context for entry points
  that don't set WorkspaceChatID explicitly (WS direct, HTTP, cron)

Fixes cross-chat doc leak where agent in chat A could see vault docs
from chat B within the same isolated team.
2026-04-23 18:43:31 +07:00

90 lines
2.5 KiB
Go

package pg
import (
"encoding/json"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// vaultDocRow is an sqlx scan struct for vault_documents SELECT queries.
// IDs are scanned as uuid.UUID then converted to string for VaultDocument.
// Metadata is scanned as raw JSON then unmarshalled post-scan.
type vaultDocRow struct {
ID uuid.UUID `db:"id"`
TenantID uuid.UUID `db:"tenant_id"`
AgentID *uuid.UUID `db:"agent_id"`
TeamID *uuid.UUID `db:"team_id"`
ChatID *string `db:"chat_id"`
Scope string `db:"scope"`
CustomScope *string `db:"custom_scope"`
Path string `db:"path"`
PathBasename string `db:"path_basename"`
Title string `db:"title"`
DocType string `db:"doc_type"`
ContentHash string `db:"content_hash"`
Summary string `db:"summary"`
MetaJSON []byte `db:"metadata"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
// toVaultDocument converts a vaultDocRow to store.VaultDocument.
func (r *vaultDocRow) toVaultDocument() store.VaultDocument {
doc := store.VaultDocument{
ID: r.ID.String(),
TenantID: r.TenantID.String(),
Scope: r.Scope,
CustomScope: r.CustomScope,
Path: r.Path,
PathBasename: r.PathBasename,
Title: r.Title,
DocType: r.DocType,
ContentHash: r.ContentHash,
Summary: r.Summary,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
if r.AgentID != nil {
s := r.AgentID.String()
doc.AgentID = &s
}
if r.TeamID != nil {
s := r.TeamID.String()
doc.TeamID = &s
}
if r.ChatID != nil {
s := *r.ChatID
doc.ChatID = &s
}
if len(r.MetaJSON) > 0 {
json.Unmarshal(r.MetaJSON, &doc.Metadata) //nolint:errcheck
}
return doc
}
// vaultDocRowsToDocs converts a slice of vaultDocRow to store.VaultDocument.
func vaultDocRowsToDocs(rows []vaultDocRow) []store.VaultDocument {
docs := make([]store.VaultDocument, len(rows))
for i := range rows {
docs[i] = rows[i].toVaultDocument()
}
return docs
}
// vaultSearchRow extends vaultDocRow with a computed score column for search queries.
type vaultSearchRow struct {
vaultDocRow
Score float64 `db:"score"`
}
// toVaultSearchResult converts a vaultSearchRow to store.VaultSearchResult.
func (r *vaultSearchRow) toVaultSearchResult(source string) store.VaultSearchResult {
return store.VaultSearchResult{
Document: r.vaultDocRow.toVaultDocument(),
Score: r.Score,
Source: source,
}
}