mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 20:15:42 +00:00
* feat(mcp): expand CRUD MCP server to near-complete CLI parity Closes the CLI-vs-MCP tool coverage gap identified by auditing every `goclaw` CLI command against the existing goclaw_* MCP tool set. Adds skill grant/revoke, agent skill pin/unpin, and full CRUD/inspection surfaces for memory, knowledge graph (including dedup/merge/prune), tenants, providers, LLM traces, channel contacts, pending messages, audit activity, system config, tenant storage (list/size/delete/move), scoped agent config export/import, secure-CLI binary registry, and a DB-backed health check. Deliberately out of scope, documented inline where relevant: - `goclaw credentials`: confirmed CLI-local (~/.goclaw/config.yaml + keychain), no server resource to wrap. goclaw_secure_cli_binaries_* covers the closest real, previously-uncovered server resource instead. - Full tar-archive agent export/import (KG + workspace files): the CLI's version streams a multi-section archive with progress events, a shape that doesn't map to a single MCP tool call. Config + context files (the portable "brain") is covered. - `kg extract` (LLM-driven text extraction): goclaw_kg_ingest accepts the same Entity/Relation shapes the extractor produces, so a caller can run extraction itself and hand off the result. Wires 8 new store dependencies (Memory, KnowledgeGraph, Tracing, Contacts, PendingMessages, Activity, SystemConfigs, SecureCLI) through gateway.Server setters -> cmd/gateway.go -> CRUDDeps, following the existing Providers/Tenants pattern. Storage and secure-CLI-binary handlers duplicate internal/http's path-escape/symlink-hiding validation logic (documented inline) since internal/http already imports internal/mcp and the reverse would cycle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(mcp): return sessionKey in goclaw_chat_send response for persistent conversations The goclaw_chat_send tool creates a new session internally when sessionKey is empty, but never returned the key to the caller. This prevented using goclaw_chat_history to fetch previous messages in the session. Add SessionKey field to ChatSendResult so callers can: 1. Start a new agent chat without providing sessionKey 2. Receive the sessionKey back in the response 3. Use that sessionKey for follow-up messages and history queries Fixes the training loop pattern: start chat → get sessionKey → call goclaw_chat_history with that key → iterate skill based on actual failures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(mcp): add timing diagnostics to goclaw_chat_send for timeout troubleshooting Log request arrival, processing duration, and errors with millisecond precision. Helps identify whether timeouts occur at MCP client→goclaw, goclaw→ollama, or during agent execution. Critical for production debugging. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix(gateway): add http.Server timeouts for defensive timeout handling Set explicit timeouts in http.Server: - ReadTimeout: 1h (allow large uploads, long-running agent operations) - WriteTimeout: 1h (allow streaming responses to slow clients) - IdleTimeout: 30s (close idle keep-alive connections quickly) Provides defense-in-depth when Nginx/Traefik timeouts are misconfigured. Matches Nginx timeout (3600s) to prevent race conditions. Timeout chain: traefik 3600s = nginx 3600s = goclaw 3600s Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> --------- Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
85 lines
2.6 KiB
Go
85 lines
2.6 KiB
Go
package skills
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// Errors returned by CreateFromContent.
|
|
var (
|
|
ErrSkillNameRequired = errors.New("name is required in SKILL.md frontmatter")
|
|
ErrSkillSlugInvalid = errors.New("invalid skill slug")
|
|
ErrSkillSlugConflict = errors.New("slug conflicts with a system skill")
|
|
ErrSkillGuardRejected = errors.New("skill content failed security scan")
|
|
)
|
|
|
|
// CreateFromContent creates a new managed skill from a single SKILL.md's
|
|
// content. This is the single-file equivalent of the web UI's ZIP-based
|
|
// skill upload (SkillsHandler.handleUpload in
|
|
// internal/http/skills_upload.go) — same frontmatter parsing, security
|
|
// guard, slug validation, and version-1 directory layout, minus multi-file
|
|
// extraction and dependency scanning (not meaningful for a text-only
|
|
// caller like an MCP tool). Name/slug/description are parsed from the
|
|
// content's YAML frontmatter.
|
|
func CreateFromContent(ctx context.Context, manage store.SkillManageStore, tenantSkillsDir, content, ownerID string) (id uuid.UUID, slug string, err error) {
|
|
if violations, safe := GuardSkillContent(content); !safe {
|
|
return uuid.Nil, "", fmt.Errorf("%w: %s", ErrSkillGuardRejected, FormatGuardViolations(violations))
|
|
}
|
|
|
|
name, description, parsedSlug, frontmatter := ParseSkillFrontmatter(content)
|
|
if name == "" {
|
|
return uuid.Nil, "", ErrSkillNameRequired
|
|
}
|
|
slug = parsedSlug
|
|
if slug == "" {
|
|
slug = Slugify(name)
|
|
}
|
|
if !SlugRegexp.MatchString(slug) {
|
|
return uuid.Nil, "", ErrSkillSlugInvalid
|
|
}
|
|
if manage.IsSystemSkill(slug) {
|
|
return uuid.Nil, "", ErrSkillSlugConflict
|
|
}
|
|
|
|
version := manage.GetNextVersion(ctx, slug)
|
|
destDir := filepath.Join(tenantSkillsDir, slug, strconv.Itoa(version))
|
|
if err := os.MkdirAll(destDir, 0o755); err != nil {
|
|
return uuid.Nil, "", err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(destDir, "SKILL.md"), []byte(content), 0o644); err != nil {
|
|
_ = os.RemoveAll(destDir)
|
|
return uuid.Nil, "", err
|
|
}
|
|
|
|
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(content)))
|
|
desc := description
|
|
id, err = manage.CreateSkillManaged(ctx, store.SkillCreateParams{
|
|
Name: name,
|
|
Slug: slug,
|
|
Description: &desc,
|
|
OwnerID: ownerID,
|
|
Visibility: "internal",
|
|
Status: "active",
|
|
Version: version,
|
|
FilePath: destDir,
|
|
FileSize: int64(len(content)),
|
|
FileHash: &hash,
|
|
Frontmatter: frontmatter,
|
|
})
|
|
if err != nil {
|
|
_ = os.RemoveAll(destDir)
|
|
return uuid.Nil, "", err
|
|
}
|
|
manage.BumpVersion()
|
|
return id, slug, nil
|
|
}
|