Files
goclaw/cmd/gateway_vault_wiring.go
Justin Truong 466ef577f7 fix(vault): resolve vault_read paths under tenant workspace root
vault_read is a singleton wired once at boot with the global workspace
root, then joins doc.Path (stored relative to the TENANT root) onto it.
For master tenant both roots coincide, so the bug stayed invisible; every
other tenant reads ENOENT because its files live under tenants/<slug>/.
vault_search is DB-only and keeps returning the doc, so the symptom is
"search finds it, read fails" rather than an obvious missing file.

Resolve the tenant-scoped root per request via TenantLayer, which is a
no-op for master and preserves existing single-tenant behaviour. Passing
the tenant root into resolvePath also narrows the boundary check, so it
now doubles as cross-tenant isolation.

Runs originating from channels or cron may not carry the tenant slug in
context; without it TenantLayer falls back to a UUID-named directory that
does not match the slug-named one the write path creates. Add a nil-safe
TenantStore lookup for that case only.
2026-08-03 22:50:12 +07:00

97 lines
3.4 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)
}
// Tenant slug fallback for runs whose context carries no slug (channels, cron).
if stores.Tenants != nil {
vaultReadTool.SetTenantStore(stores.Tenants)
}
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
}