Files
goclaw/cmd/gateway_vault_wiring.go
T
Plateau Nguyenandviettranx 6d7389539a fix(vault): prevent vault_read id-namespace collision (#959)
* fix(vault): prevent vault_read id-namespace collision

vault_search was leaking KG/episodic entity ids into result sets even when
narrow `types` were requested, and callers then passed those ids to
vault_read which returned a generic "document not found". The cause was
threefold:

1. `types` filter was only applied to the vault fan-out; KG and episodic
   ran unconditionally. Now gated by shouldFanout(types, key).
2. vault_search output lacked a per-source tool hint. Each result now ends
   with " → use <tool>" naming the correct follow-up (vault_read,
   knowledge_graph_search, or memory_search).
3. vault_read miss returned "document not found" without checking whether
   the id belonged to a foreign namespace. It now probes KG then episodic
   and returns a namespace-specific redirect error. Stores are injected
   via SetKGStore/SetEpisodicStore, nil-safe, tenant-scoped.

Adds red→green characterization tests plus an end-to-end integration
scenario seeding a vault doc + KG entity with identical basenames.

* test(agent): bump none-mode prompt size budget to 3100

vault_read wiring (#948) added ~95 chars to read_file tool summary,
pushing none-mode prompt from <3000 to 3075 chars. Bump budget to
3100 (~775 tokens) to match the intentional addition.

* test(integration): ensure data_migrations table exists in reset helper

The reset helper runs before RunPendingHooks, but RunPendingHooks is
what normally creates data_migrations. On a fresh CI database the
DELETE fails with 'relation does not exist'. Create the table
defensively so reset works regardless of execution order.

* refactor(vault): per-source id fields + wire episodic into search

Align vault_search output fields with downstream tool input params:
doc_id (vault_read), entity_id (knowledge_graph_search), episodic_id
(memory_expand). Prevents LLMs from pattern-matching a generic `id:`
and misrouting a foreign-namespace uuid into vault_read. Fallback
redirect in vault_read now quotes id + names the correct param so
the LLM can self-correct in one turn.

Also wire stores.Episodic into VaultSearchService (stale comment
claimed pending-impl; PGEpisodicStore has existed and been in use
since v3). Unifies search fan-out with vault_read namespace probe.

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-04-19 15:00:07 +07:00

93 lines
3.3 KiB
Go

package cmd
import (
"log/slog"
"github.com/nextlevelbuilder/goclaw/internal/eventbus"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/internal/tools"
"github.com/nextlevelbuilder/goclaw/internal/vault"
)
// wireVault wires Knowledge Vault tools and interceptors into the tool registry.
// All wiring is skipped if stores.Vault is nil.
// Pattern mirrors wireExtras KG wiring: register tools, set stores, set interceptors.
// wireVault wires Knowledge Vault tools and interceptors into the tool registry.
// Returns the shared VaultInterceptor for use by other subsystems (e.g. agent upload hook).
// Returns nil if stores.Vault is nil.
func wireVault(stores *store.Stores, toolsReg *tools.Registry, workspace string, bus eventbus.DomainEventBus) *tools.VaultInterceptor {
if stores.Vault == nil {
return nil
}
// Register vault tools — these are always available when vault store is present.
vaultSearchTool := tools.NewVaultSearchTool()
toolsReg.Register(vaultSearchTool)
// vault_read: fetch full content of a vault doc by doc_id (chained from vault_search).
vaultReadTool := tools.NewVaultReadTool()
vaultReadTool.SetVaultStore(stores.Vault)
vaultReadTool.SetWorkspace(workspace)
// Namespace-fallback stores — nil-safe; used to return a redirect error
// when a caller passes a KG / episodic id to vault_read by mistake.
if stores.KnowledgeGraph != nil {
vaultReadTool.SetKGStore(stores.KnowledgeGraph)
}
if stores.Episodic != nil {
vaultReadTool.SetEpisodicStore(stores.Episodic)
}
toolsReg.Register(vaultReadTool)
// Build VaultSearchService: fan-out across vault + episodic + KG.
// Each store is nil-safe inside the service (skipped when absent).
searchSvc := vault.NewVaultSearchService(stores.Vault, stores.Episodic, stores.KnowledgeGraph)
vaultSearchTool.SetSearchService(searchSvc)
// Build shared VaultInterceptor for read/write tool vault registration.
vaultIntc := tools.NewVaultInterceptor(stores.Vault, workspace, bus)
// Wire interceptor into write_file (registers doc on write).
if writeTool, ok := toolsReg.Get("write_file"); ok {
if wt, ok := writeTool.(*tools.WriteFileTool); ok {
wt.SetVaultInterceptor(vaultIntc)
}
}
// Wire interceptor into read_file (lazy hash sync on read).
if readTool, ok := toolsReg.Get("read_file"); ok {
if rt, ok := readTool.(*tools.ReadFileTool); ok {
rt.SetVaultInterceptor(vaultIntc)
}
}
// Wire interceptor into media generation tools.
if imgTool, ok := toolsReg.Get("create_image"); ok {
if it, ok := imgTool.(*tools.CreateImageTool); ok {
it.SetVaultInterceptor(vaultIntc)
}
}
if vidTool, ok := toolsReg.Get("create_video"); ok {
if vt, ok := vidTool.(*tools.CreateVideoTool); ok {
vt.SetVaultInterceptor(vaultIntc)
}
}
if audTool, ok := toolsReg.Get("create_audio"); ok {
if at, ok := audTool.(*tools.CreateAudioTool); ok {
at.SetVaultInterceptor(vaultIntc)
}
}
if ttsTool, ok := toolsReg.Get("tts"); ok {
if tt, ok := ttsTool.(*tools.TtsTool); ok {
tt.SetVaultInterceptor(vaultIntc)
}
}
if editTool, ok := toolsReg.Get("edit"); ok {
if et, ok := editTool.(*tools.EditTool); ok {
et.SetVaultInterceptor(vaultIntc)
}
}
slog.Info("vault tools registered", "tools", "vault_search,vault_read,create_image,create_video,create_audio,tts,edit")
return vaultIntc
}