fix(mcp): unified grant revocation with per-agent tool group isolation

- Add GrantChecker interface with cache + event-bus invalidation for revalidating MCP tool grants at execution time (not just dispatch)
- Move toolGroups from package-level global to per-Registry field; Clone() gives each agent Loop isolated tool groups, eliminating cross-agent race condition
- BridgeTool.Execute rechecks grant validity before executing MCP methods; uses serverID field for cache key
- Wire MCPGrantChecker through ResolverDeps to MCP Manager and LoopConfig
- Add integration test for grant revocation scenarios
- Update policy functions to accept *Registry parameter
This commit is contained in:
viettranx committed 2026-04-16 17:07:56 +07:00
1 parent 8618ed94b2
commit 8b8da3a3dd
20 files changed
+1128 -150

No files matched your search

+14
View File
@@ -15,6 +15,7 @@ import (
"github.com/nextlevelbuilder/goclaw/internal/bus"
"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/i18n"
"github.com/nextlevelbuilder/goclaw/internal/skills"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
)
@@ -132,6 +133,19 @@ func (h *SkillsHandler) doSkillsImport(ctx context.Context, r io.Reader, userID
continue
}
// Security guard: scan SKILL.md for malicious content BEFORE any disk/DB write
if entry.skillMD != nil {
violations, safe := skills.GuardSkillContent(string(entry.skillMD))
if !safe {
slog.Warn("security.skills.import_rejected",
"slug", slug,
"violations", len(violations),
"first_rule", violations[0].Reason)
summary.SkillsSkipped++
continue
}
}
var meta struct {
Name string `json:"name"`
Slug string `json:"slug"`
+14
View File
@@ -106,6 +106,20 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
return
}
// Security guard: scan for malicious content BEFORE any disk/DB write
violations, safe := skills.GuardSkillContent(skillContent)
if !safe {
slog.Warn("security.skills.upload_rejected",
"user_id", userID,
"violations", len(violations),
"first_rule", violations[0].Reason)
writeJSON(w, http.StatusBadRequest, map[string]any{
"error": i18n.T(locale, i18n.MsgInvalidRequest, "skill content failed security scan"),
"violations": skills.FormatGuardViolations(violations),
})
return
}
name, description, slug, frontmatter := skills.ParseSkillFrontmatter(skillContent)
if name == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "name in SKILL.md frontmatter")})