feat: add agent skills and guardrails functionality
- Introduced new tools for managing agent skills and guardrails, including add, remove, and list operations. - Updated README.md to document new commands and usage patterns for skills and guardrails. - Enhanced server configuration to support longer read and write timeouts. - Increased maximum upload size for files to 100 MB and adjusted related configurations. - Created database migrations for agent skills, guardrails, and their associations with projects. - Updated relevant code files to integrate new skills and guardrails into the application logic.
This commit is contained in:
78
README.md
78
README.md
@@ -47,6 +47,51 @@ A Go MCP server for capturing and retrieving thoughts, memory, and project conte
|
|||||||
| `backfill_embeddings` | Generate missing embeddings for stored thoughts |
|
| `backfill_embeddings` | Generate missing embeddings for stored thoughts |
|
||||||
| `reparse_thought_metadata` | Re-extract and normalize metadata for stored thoughts |
|
| `reparse_thought_metadata` | Re-extract and normalize metadata for stored thoughts |
|
||||||
| `retry_failed_metadata` | Retry metadata extraction for thoughts still pending or failed |
|
| `retry_failed_metadata` | Retry metadata extraction for thoughts still pending or failed |
|
||||||
|
| `add_skill` | Store a reusable agent skill (behavioural instruction or capability prompt) |
|
||||||
|
| `remove_skill` | Delete an agent skill by id |
|
||||||
|
| `list_skills` | List all agent skills, optionally filtered by tag |
|
||||||
|
| `add_guardrail` | Store a reusable agent guardrail (constraint or safety rule) |
|
||||||
|
| `remove_guardrail` | Delete an agent guardrail by id |
|
||||||
|
| `list_guardrails` | List all agent guardrails, optionally filtered by tag or severity |
|
||||||
|
| `add_project_skill` | Link an agent skill to a project |
|
||||||
|
| `remove_project_skill` | Unlink an agent skill from a project |
|
||||||
|
| `list_project_skills` | List all skills linked to a project |
|
||||||
|
| `add_project_guardrail` | Link an agent guardrail to a project |
|
||||||
|
| `remove_project_guardrail` | Unlink an agent guardrail from a project |
|
||||||
|
| `list_project_guardrails` | List all guardrails linked to a project |
|
||||||
|
|
||||||
|
## Agent Skills and Guardrails
|
||||||
|
|
||||||
|
Skills and guardrails are reusable agent behaviour instructions and constraints that can be attached to projects.
|
||||||
|
|
||||||
|
**At the start of every project session, always call `list_project_skills` and `list_project_guardrails` first.** Use the returned skills and guardrails to guide agent behaviour for that project. Only generate or create new skills/guardrails if none are returned.
|
||||||
|
|
||||||
|
### Skills
|
||||||
|
|
||||||
|
A skill is a reusable behavioural instruction or capability prompt — for example, "always respond in structured markdown" or "break complex tasks into numbered steps before starting".
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "structured-output", "description": "Enforce markdown output format", "content": "Always structure responses using markdown headers and bullet points.", "tags": ["formatting"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Guardrails
|
||||||
|
|
||||||
|
A guardrail is a constraint or safety rule — for example, "never delete files without explicit confirmation" or "do not expose secrets in output".
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "no-silent-deletes", "description": "Require confirmation before deletes", "content": "Never delete, drop, or truncate data without first confirming with the user.", "severity": "high", "tags": ["safety"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Severity levels: `low`, `medium`, `high`, `critical`.
|
||||||
|
|
||||||
|
### Project linking
|
||||||
|
|
||||||
|
Link existing skills and guardrails to a project so they are automatically available when that project is active:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "project": "my-project", "skill_id": "<uuid>" }
|
||||||
|
{ "project": "my-project", "guardrail_id": "<uuid>" }
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -169,6 +214,8 @@ List files for a thought or project with:
|
|||||||
|
|
||||||
AMCS also supports direct authenticated HTTP uploads to `/files` for clients that want to stream file bodies instead of base64-encoding them into an MCP tool call.
|
AMCS also supports direct authenticated HTTP uploads to `/files` for clients that want to stream file bodies instead of base64-encoding them into an MCP tool call.
|
||||||
|
|
||||||
|
The Go server caps `/files` uploads at 100 MB per request. Large uploads are still also subject to available memory, Postgres limits, and any reverse proxy or load balancer limits in front of AMCS.
|
||||||
|
|
||||||
Multipart upload:
|
Multipart upload:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -262,6 +309,37 @@ Or add directly to `opencode.json` / `~/.config/opencode/config.json`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Apache Proxy
|
||||||
|
|
||||||
|
If AMCS is deployed behind Apache HTTP Server, configure the proxy explicitly for larger uploads and longer-running requests.
|
||||||
|
|
||||||
|
Example virtual host settings for the current AMCS defaults:
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName amcs.example.com
|
||||||
|
|
||||||
|
ProxyPreserveHost On
|
||||||
|
LimitRequestBody 104857600
|
||||||
|
RequestReadTimeout handshake=0 header=20-40,MinRate=500 body=600,MinRate=500
|
||||||
|
Timeout 600
|
||||||
|
ProxyTimeout 600
|
||||||
|
|
||||||
|
ProxyPass /mcp http://127.0.0.1:8080/mcp connectiontimeout=30 timeout=600
|
||||||
|
ProxyPassReverse /mcp http://127.0.0.1:8080/mcp
|
||||||
|
|
||||||
|
ProxyPass /files http://127.0.0.1:8080/files connectiontimeout=30 timeout=600
|
||||||
|
ProxyPassReverse /files http://127.0.0.1:8080/files
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended Apache settings:
|
||||||
|
|
||||||
|
- `LimitRequestBody 104857600` matches AMCS's 100 MB `/files` upload cap.
|
||||||
|
- `RequestReadTimeout ... body=600` gives clients up to 10 minutes to send larger request bodies.
|
||||||
|
- `ProxyTimeout 600` and `ProxyPass ... timeout=600` give Apache enough time to wait for the Go backend.
|
||||||
|
- If another proxy or load balancer sits in front of Apache, align its size and timeout settings too.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Run the SQL migrations against a local database with:
|
Run the SQL migrations against a local database with:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
server:
|
server:
|
||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
port: 8080
|
port: 8080
|
||||||
read_timeout: "15s"
|
read_timeout: "10m"
|
||||||
write_timeout: "30s"
|
write_timeout: "10m"
|
||||||
idle_timeout: "60s"
|
idle_timeout: "60s"
|
||||||
allowed_origins:
|
allowed_origins:
|
||||||
- "*"
|
- "*"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
server:
|
server:
|
||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
port: 8080
|
port: 8080
|
||||||
read_timeout: "15s"
|
read_timeout: "10m"
|
||||||
write_timeout: "30s"
|
write_timeout: "10m"
|
||||||
idle_timeout: "60s"
|
idle_timeout: "60s"
|
||||||
allowed_origins:
|
allowed_origins:
|
||||||
- "*"
|
- "*"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
server:
|
server:
|
||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
port: 8080
|
port: 8080
|
||||||
read_timeout: "15s"
|
read_timeout: "10m"
|
||||||
write_timeout: "30s"
|
write_timeout: "10m"
|
||||||
idle_timeout: "60s"
|
idle_timeout: "60s"
|
||||||
allowed_origins:
|
allowed_origins:
|
||||||
- "*"
|
- "*"
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ func routes(logger *slog.Logger, cfg *config.Config, db *store.DB, provider ai.P
|
|||||||
Calendar: tools.NewCalendarTool(db),
|
Calendar: tools.NewCalendarTool(db),
|
||||||
Meals: tools.NewMealsTool(db),
|
Meals: tools.NewMealsTool(db),
|
||||||
CRM: tools.NewCRMTool(db),
|
CRM: tools.NewCRMTool(db),
|
||||||
|
Skills: tools.NewSkillsTool(db, activeProjects),
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpHandler := mcpserver.New(cfg.MCP, toolSet)
|
mcpHandler := mcpserver.New(cfg.MCP, toolSet)
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import (
|
|||||||
"git.warky.dev/wdevs/amcs/internal/tools"
|
"git.warky.dev/wdevs/amcs/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxUploadBytes = 50 << 20
|
const (
|
||||||
|
maxUploadBytes = 100 << 20
|
||||||
|
multipartFormMemory = 32 << 20
|
||||||
|
)
|
||||||
|
|
||||||
func fileUploadHandler(files *tools.FilesTool) http.Handler {
|
func fileUploadHandler(files *tools.FilesTool) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -53,7 +56,7 @@ func parseUploadRequest(r *http.Request) (tools.SaveFileDecodedInput, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseMultipartUpload(r *http.Request) (tools.SaveFileDecodedInput, error) {
|
func parseMultipartUpload(r *http.Request) (tools.SaveFileDecodedInput, error) {
|
||||||
if err := r.ParseMultipartForm(maxUploadBytes); err != nil {
|
if err := r.ParseMultipartForm(multipartFormMemory); err != nil {
|
||||||
return tools.SaveFileDecodedInput{}, err
|
return tools.SaveFileDecodedInput{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ func defaultConfig() Config {
|
|||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Host: "0.0.0.0",
|
Host: "0.0.0.0",
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
ReadTimeout: 15 * time.Second,
|
ReadTimeout: 10 * time.Minute,
|
||||||
WriteTimeout: 30 * time.Second,
|
WriteTimeout: 10 * time.Minute,
|
||||||
IdleTimeout: 60 * time.Second,
|
IdleTimeout: 60 * time.Second,
|
||||||
},
|
},
|
||||||
MCP: MCPConfig{
|
MCP: MCPConfig{
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type ToolSet struct {
|
|||||||
Calendar *tools.CalendarTool
|
Calendar *tools.CalendarTool
|
||||||
Meals *tools.MealsTool
|
Meals *tools.MealsTool
|
||||||
CRM *tools.CRMTool
|
CRM *tools.CRMTool
|
||||||
|
Skills *tools.SkillsTool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg config.MCPConfig, toolSet ToolSet) http.Handler {
|
func New(cfg config.MCPConfig, toolSet ToolSet) http.Handler {
|
||||||
@@ -306,6 +307,69 @@ func New(cfg config.MCPConfig, toolSet ToolSet) http.Handler {
|
|||||||
Description: "Append a stored thought to a contact's notes.",
|
Description: "Append a stored thought to a contact's notes.",
|
||||||
}, toolSet.CRM.LinkThought)
|
}, toolSet.CRM.LinkThought)
|
||||||
|
|
||||||
|
// Agent Skills
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "add_skill",
|
||||||
|
Description: "Store a reusable agent skill (behavioural instruction or capability prompt).",
|
||||||
|
}, toolSet.Skills.AddSkill)
|
||||||
|
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "remove_skill",
|
||||||
|
Description: "Delete an agent skill by id.",
|
||||||
|
}, toolSet.Skills.RemoveSkill)
|
||||||
|
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "list_skills",
|
||||||
|
Description: "List all agent skills, optionally filtered by tag.",
|
||||||
|
}, toolSet.Skills.ListSkills)
|
||||||
|
|
||||||
|
// Agent Guardrails
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "add_guardrail",
|
||||||
|
Description: "Store a reusable agent guardrail (constraint or safety rule).",
|
||||||
|
}, toolSet.Skills.AddGuardrail)
|
||||||
|
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "remove_guardrail",
|
||||||
|
Description: "Delete an agent guardrail by id.",
|
||||||
|
}, toolSet.Skills.RemoveGuardrail)
|
||||||
|
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "list_guardrails",
|
||||||
|
Description: "List all agent guardrails, optionally filtered by tag or severity.",
|
||||||
|
}, toolSet.Skills.ListGuardrails)
|
||||||
|
|
||||||
|
// Project Skills & Guardrails
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "add_project_skill",
|
||||||
|
Description: "Link an agent skill to a project.",
|
||||||
|
}, toolSet.Skills.AddProjectSkill)
|
||||||
|
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "remove_project_skill",
|
||||||
|
Description: "Unlink an agent skill from a project.",
|
||||||
|
}, toolSet.Skills.RemoveProjectSkill)
|
||||||
|
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "list_project_skills",
|
||||||
|
Description: "List all skills linked to a project. Call this at the start of a project session to load existing agent behaviour instructions before generating new ones.",
|
||||||
|
}, toolSet.Skills.ListProjectSkills)
|
||||||
|
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "add_project_guardrail",
|
||||||
|
Description: "Link an agent guardrail to a project.",
|
||||||
|
}, toolSet.Skills.AddProjectGuardrail)
|
||||||
|
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "remove_project_guardrail",
|
||||||
|
Description: "Unlink an agent guardrail from a project.",
|
||||||
|
}, toolSet.Skills.RemoveProjectGuardrail)
|
||||||
|
|
||||||
|
addTool(server, &mcp.Tool{
|
||||||
|
Name: "list_project_guardrails",
|
||||||
|
Description: "List all guardrails linked to a project. Call this at the start of a project session to load existing agent constraints before generating new ones.",
|
||||||
|
}, toolSet.Skills.ListProjectGuardrails)
|
||||||
|
|
||||||
return mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
|
return mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
|
||||||
return server
|
return server
|
||||||
}, &mcp.StreamableHTTPOptions{
|
}, &mcp.StreamableHTTPOptions{
|
||||||
|
|||||||
294
internal/store/skills.go
Normal file
294
internal/store/skills.go
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
ext "git.warky.dev/wdevs/amcs/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Agent Skills
|
||||||
|
|
||||||
|
func (db *DB) AddSkill(ctx context.Context, skill ext.AgentSkill) (ext.AgentSkill, error) {
|
||||||
|
if skill.Tags == nil {
|
||||||
|
skill.Tags = []string{}
|
||||||
|
}
|
||||||
|
row := db.pool.QueryRow(ctx, `
|
||||||
|
insert into agent_skills (name, description, content, tags)
|
||||||
|
values ($1, $2, $3, $4)
|
||||||
|
returning id, created_at, updated_at
|
||||||
|
`, skill.Name, skill.Description, skill.Content, skill.Tags)
|
||||||
|
|
||||||
|
created := skill
|
||||||
|
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
|
||||||
|
return ext.AgentSkill{}, fmt.Errorf("insert agent skill: %w", err)
|
||||||
|
}
|
||||||
|
return created, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) RemoveSkill(ctx context.Context, id uuid.UUID) error {
|
||||||
|
tag, err := db.pool.Exec(ctx, `delete from agent_skills where id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete agent skill: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("skill not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) ListSkills(ctx context.Context, tag string) ([]ext.AgentSkill, error) {
|
||||||
|
q := `select id, name, description, content, tags, created_at, updated_at from agent_skills`
|
||||||
|
args := []any{}
|
||||||
|
if t := strings.TrimSpace(tag); t != "" {
|
||||||
|
args = append(args, t)
|
||||||
|
q += fmt.Sprintf(" where $%d = any(tags)", len(args))
|
||||||
|
}
|
||||||
|
q += " order by name"
|
||||||
|
|
||||||
|
rows, err := db.pool.Query(ctx, q, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list agent skills: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var skills []ext.AgentSkill
|
||||||
|
for rows.Next() {
|
||||||
|
var s ext.AgentSkill
|
||||||
|
var desc *string
|
||||||
|
if err := rows.Scan(&s.ID, &s.Name, &desc, &s.Content, &s.Tags, &s.CreatedAt, &s.UpdatedAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan agent skill: %w", err)
|
||||||
|
}
|
||||||
|
s.Description = strVal(desc)
|
||||||
|
if s.Tags == nil {
|
||||||
|
s.Tags = []string{}
|
||||||
|
}
|
||||||
|
skills = append(skills, s)
|
||||||
|
}
|
||||||
|
return skills, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetSkill(ctx context.Context, id uuid.UUID) (ext.AgentSkill, error) {
|
||||||
|
row := db.pool.QueryRow(ctx, `
|
||||||
|
select id, name, description, content, tags, created_at, updated_at
|
||||||
|
from agent_skills where id = $1
|
||||||
|
`, id)
|
||||||
|
|
||||||
|
var s ext.AgentSkill
|
||||||
|
var desc *string
|
||||||
|
if err := row.Scan(&s.ID, &s.Name, &desc, &s.Content, &s.Tags, &s.CreatedAt, &s.UpdatedAt); err != nil {
|
||||||
|
return ext.AgentSkill{}, fmt.Errorf("get agent skill: %w", err)
|
||||||
|
}
|
||||||
|
s.Description = strVal(desc)
|
||||||
|
if s.Tags == nil {
|
||||||
|
s.Tags = []string{}
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agent Guardrails
|
||||||
|
|
||||||
|
func (db *DB) AddGuardrail(ctx context.Context, g ext.AgentGuardrail) (ext.AgentGuardrail, error) {
|
||||||
|
if g.Tags == nil {
|
||||||
|
g.Tags = []string{}
|
||||||
|
}
|
||||||
|
if g.Severity == "" {
|
||||||
|
g.Severity = "medium"
|
||||||
|
}
|
||||||
|
row := db.pool.QueryRow(ctx, `
|
||||||
|
insert into agent_guardrails (name, description, content, severity, tags)
|
||||||
|
values ($1, $2, $3, $4, $5)
|
||||||
|
returning id, created_at, updated_at
|
||||||
|
`, g.Name, g.Description, g.Content, g.Severity, g.Tags)
|
||||||
|
|
||||||
|
created := g
|
||||||
|
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
|
||||||
|
return ext.AgentGuardrail{}, fmt.Errorf("insert agent guardrail: %w", err)
|
||||||
|
}
|
||||||
|
return created, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) RemoveGuardrail(ctx context.Context, id uuid.UUID) error {
|
||||||
|
tag, err := db.pool.Exec(ctx, `delete from agent_guardrails where id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete agent guardrail: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("guardrail not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) ListGuardrails(ctx context.Context, tag, severity string) ([]ext.AgentGuardrail, error) {
|
||||||
|
args := []any{}
|
||||||
|
conditions := []string{}
|
||||||
|
|
||||||
|
if t := strings.TrimSpace(tag); t != "" {
|
||||||
|
args = append(args, t)
|
||||||
|
conditions = append(conditions, fmt.Sprintf("$%d = any(tags)", len(args)))
|
||||||
|
}
|
||||||
|
if s := strings.TrimSpace(severity); s != "" {
|
||||||
|
args = append(args, s)
|
||||||
|
conditions = append(conditions, fmt.Sprintf("severity = $%d", len(args)))
|
||||||
|
}
|
||||||
|
|
||||||
|
q := `select id, name, description, content, severity, tags, created_at, updated_at from agent_guardrails`
|
||||||
|
if len(conditions) > 0 {
|
||||||
|
q += " where " + strings.Join(conditions, " and ")
|
||||||
|
}
|
||||||
|
q += " order by name"
|
||||||
|
|
||||||
|
rows, err := db.pool.Query(ctx, q, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list agent guardrails: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var guardrails []ext.AgentGuardrail
|
||||||
|
for rows.Next() {
|
||||||
|
var g ext.AgentGuardrail
|
||||||
|
var desc *string
|
||||||
|
if err := rows.Scan(&g.ID, &g.Name, &desc, &g.Content, &g.Severity, &g.Tags, &g.CreatedAt, &g.UpdatedAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan agent guardrail: %w", err)
|
||||||
|
}
|
||||||
|
g.Description = strVal(desc)
|
||||||
|
if g.Tags == nil {
|
||||||
|
g.Tags = []string{}
|
||||||
|
}
|
||||||
|
guardrails = append(guardrails, g)
|
||||||
|
}
|
||||||
|
return guardrails, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetGuardrail(ctx context.Context, id uuid.UUID) (ext.AgentGuardrail, error) {
|
||||||
|
row := db.pool.QueryRow(ctx, `
|
||||||
|
select id, name, description, content, severity, tags, created_at, updated_at
|
||||||
|
from agent_guardrails where id = $1
|
||||||
|
`, id)
|
||||||
|
|
||||||
|
var g ext.AgentGuardrail
|
||||||
|
var desc *string
|
||||||
|
if err := row.Scan(&g.ID, &g.Name, &desc, &g.Content, &g.Severity, &g.Tags, &g.CreatedAt, &g.UpdatedAt); err != nil {
|
||||||
|
return ext.AgentGuardrail{}, fmt.Errorf("get agent guardrail: %w", err)
|
||||||
|
}
|
||||||
|
g.Description = strVal(desc)
|
||||||
|
if g.Tags == nil {
|
||||||
|
g.Tags = []string{}
|
||||||
|
}
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project Skills
|
||||||
|
|
||||||
|
func (db *DB) AddProjectSkill(ctx context.Context, projectID, skillID uuid.UUID) error {
|
||||||
|
_, err := db.pool.Exec(ctx, `
|
||||||
|
insert into project_skills (project_id, skill_id)
|
||||||
|
values ($1, $2)
|
||||||
|
on conflict do nothing
|
||||||
|
`, projectID, skillID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add project skill: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) RemoveProjectSkill(ctx context.Context, projectID, skillID uuid.UUID) error {
|
||||||
|
tag, err := db.pool.Exec(ctx, `
|
||||||
|
delete from project_skills where project_id = $1 and skill_id = $2
|
||||||
|
`, projectID, skillID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove project skill: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("project skill link not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) ListProjectSkills(ctx context.Context, projectID uuid.UUID) ([]ext.AgentSkill, error) {
|
||||||
|
rows, err := db.pool.Query(ctx, `
|
||||||
|
select s.id, s.name, s.description, s.content, s.tags, s.created_at, s.updated_at
|
||||||
|
from agent_skills s
|
||||||
|
join project_skills ps on ps.skill_id = s.id
|
||||||
|
where ps.project_id = $1
|
||||||
|
order by s.name
|
||||||
|
`, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list project skills: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var skills []ext.AgentSkill
|
||||||
|
for rows.Next() {
|
||||||
|
var s ext.AgentSkill
|
||||||
|
var desc *string
|
||||||
|
if err := rows.Scan(&s.ID, &s.Name, &desc, &s.Content, &s.Tags, &s.CreatedAt, &s.UpdatedAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan project skill: %w", err)
|
||||||
|
}
|
||||||
|
s.Description = strVal(desc)
|
||||||
|
if s.Tags == nil {
|
||||||
|
s.Tags = []string{}
|
||||||
|
}
|
||||||
|
skills = append(skills, s)
|
||||||
|
}
|
||||||
|
return skills, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project Guardrails
|
||||||
|
|
||||||
|
func (db *DB) AddProjectGuardrail(ctx context.Context, projectID, guardrailID uuid.UUID) error {
|
||||||
|
_, err := db.pool.Exec(ctx, `
|
||||||
|
insert into project_guardrails (project_id, guardrail_id)
|
||||||
|
values ($1, $2)
|
||||||
|
on conflict do nothing
|
||||||
|
`, projectID, guardrailID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add project guardrail: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) RemoveProjectGuardrail(ctx context.Context, projectID, guardrailID uuid.UUID) error {
|
||||||
|
tag, err := db.pool.Exec(ctx, `
|
||||||
|
delete from project_guardrails where project_id = $1 and guardrail_id = $2
|
||||||
|
`, projectID, guardrailID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove project guardrail: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("project guardrail link not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) ListProjectGuardrails(ctx context.Context, projectID uuid.UUID) ([]ext.AgentGuardrail, error) {
|
||||||
|
rows, err := db.pool.Query(ctx, `
|
||||||
|
select g.id, g.name, g.description, g.content, g.severity, g.tags, g.created_at, g.updated_at
|
||||||
|
from agent_guardrails g
|
||||||
|
join project_guardrails pg on pg.guardrail_id = g.id
|
||||||
|
where pg.project_id = $1
|
||||||
|
order by g.name
|
||||||
|
`, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list project guardrails: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var guardrails []ext.AgentGuardrail
|
||||||
|
for rows.Next() {
|
||||||
|
var g ext.AgentGuardrail
|
||||||
|
var desc *string
|
||||||
|
if err := rows.Scan(&g.ID, &g.Name, &desc, &g.Content, &g.Severity, &g.Tags, &g.CreatedAt, &g.UpdatedAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan project guardrail: %w", err)
|
||||||
|
}
|
||||||
|
g.Description = strVal(desc)
|
||||||
|
if g.Tags == nil {
|
||||||
|
g.Tags = []string{}
|
||||||
|
}
|
||||||
|
guardrails = append(guardrails, g)
|
||||||
|
}
|
||||||
|
return guardrails, rows.Err()
|
||||||
|
}
|
||||||
322
internal/tools/skills.go
Normal file
322
internal/tools/skills.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/session"
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/store"
|
||||||
|
ext "git.warky.dev/wdevs/amcs/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SkillsTool struct {
|
||||||
|
store *store.DB
|
||||||
|
sessions *session.ActiveProjects
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSkillsTool(db *store.DB, sessions *session.ActiveProjects) *SkillsTool {
|
||||||
|
return &SkillsTool{store: db, sessions: sessions}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add_skill
|
||||||
|
|
||||||
|
type AddSkillInput struct {
|
||||||
|
Name string `json:"name" jsonschema:"unique skill name"`
|
||||||
|
Description string `json:"description,omitempty" jsonschema:"short description of what the skill does"`
|
||||||
|
Content string `json:"content" jsonschema:"the full skill instruction or prompt content"`
|
||||||
|
Tags []string `json:"tags,omitempty" jsonschema:"optional tags for grouping or filtering"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddSkillOutput struct {
|
||||||
|
Skill ext.AgentSkill `json:"skill"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) AddSkill(ctx context.Context, _ *mcp.CallToolRequest, in AddSkillInput) (*mcp.CallToolResult, AddSkillOutput, error) {
|
||||||
|
if strings.TrimSpace(in.Name) == "" {
|
||||||
|
return nil, AddSkillOutput{}, errInvalidInput("name is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(in.Content) == "" {
|
||||||
|
return nil, AddSkillOutput{}, errInvalidInput("content is required")
|
||||||
|
}
|
||||||
|
if in.Tags == nil {
|
||||||
|
in.Tags = []string{}
|
||||||
|
}
|
||||||
|
skill, err := t.store.AddSkill(ctx, ext.AgentSkill{
|
||||||
|
Name: strings.TrimSpace(in.Name),
|
||||||
|
Description: strings.TrimSpace(in.Description),
|
||||||
|
Content: strings.TrimSpace(in.Content),
|
||||||
|
Tags: in.Tags,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, AddSkillOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, AddSkillOutput{Skill: skill}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove_skill
|
||||||
|
|
||||||
|
type RemoveSkillInput struct {
|
||||||
|
ID uuid.UUID `json:"id" jsonschema:"skill id to remove"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveSkillOutput struct {
|
||||||
|
Removed bool `json:"removed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) RemoveSkill(ctx context.Context, _ *mcp.CallToolRequest, in RemoveSkillInput) (*mcp.CallToolResult, RemoveSkillOutput, error) {
|
||||||
|
if err := t.store.RemoveSkill(ctx, in.ID); err != nil {
|
||||||
|
return nil, RemoveSkillOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, RemoveSkillOutput{Removed: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// list_skills
|
||||||
|
|
||||||
|
type ListSkillsInput struct {
|
||||||
|
Tag string `json:"tag,omitempty" jsonschema:"filter by tag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListSkillsOutput struct {
|
||||||
|
Skills []ext.AgentSkill `json:"skills"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) ListSkills(ctx context.Context, _ *mcp.CallToolRequest, in ListSkillsInput) (*mcp.CallToolResult, ListSkillsOutput, error) {
|
||||||
|
skills, err := t.store.ListSkills(ctx, in.Tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListSkillsOutput{}, err
|
||||||
|
}
|
||||||
|
if skills == nil {
|
||||||
|
skills = []ext.AgentSkill{}
|
||||||
|
}
|
||||||
|
return nil, ListSkillsOutput{Skills: skills}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// add_guardrail
|
||||||
|
|
||||||
|
type AddGuardrailInput struct {
|
||||||
|
Name string `json:"name" jsonschema:"unique guardrail name"`
|
||||||
|
Description string `json:"description,omitempty" jsonschema:"short description of what the guardrail enforces"`
|
||||||
|
Content string `json:"content" jsonschema:"the full guardrail rule or constraint content"`
|
||||||
|
Severity string `json:"severity,omitempty" jsonschema:"one of: low, medium, high, critical (default medium)"`
|
||||||
|
Tags []string `json:"tags,omitempty" jsonschema:"optional tags for grouping or filtering"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddGuardrailOutput struct {
|
||||||
|
Guardrail ext.AgentGuardrail `json:"guardrail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) AddGuardrail(ctx context.Context, _ *mcp.CallToolRequest, in AddGuardrailInput) (*mcp.CallToolResult, AddGuardrailOutput, error) {
|
||||||
|
if strings.TrimSpace(in.Name) == "" {
|
||||||
|
return nil, AddGuardrailOutput{}, errInvalidInput("name is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(in.Content) == "" {
|
||||||
|
return nil, AddGuardrailOutput{}, errInvalidInput("content is required")
|
||||||
|
}
|
||||||
|
severity := strings.TrimSpace(in.Severity)
|
||||||
|
if severity == "" {
|
||||||
|
severity = "medium"
|
||||||
|
}
|
||||||
|
switch severity {
|
||||||
|
case "low", "medium", "high", "critical":
|
||||||
|
default:
|
||||||
|
return nil, AddGuardrailOutput{}, errInvalidInput("severity must be one of: low, medium, high, critical")
|
||||||
|
}
|
||||||
|
if in.Tags == nil {
|
||||||
|
in.Tags = []string{}
|
||||||
|
}
|
||||||
|
guardrail, err := t.store.AddGuardrail(ctx, ext.AgentGuardrail{
|
||||||
|
Name: strings.TrimSpace(in.Name),
|
||||||
|
Description: strings.TrimSpace(in.Description),
|
||||||
|
Content: strings.TrimSpace(in.Content),
|
||||||
|
Severity: severity,
|
||||||
|
Tags: in.Tags,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, AddGuardrailOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, AddGuardrailOutput{Guardrail: guardrail}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove_guardrail
|
||||||
|
|
||||||
|
type RemoveGuardrailInput struct {
|
||||||
|
ID uuid.UUID `json:"id" jsonschema:"guardrail id to remove"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveGuardrailOutput struct {
|
||||||
|
Removed bool `json:"removed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) RemoveGuardrail(ctx context.Context, _ *mcp.CallToolRequest, in RemoveGuardrailInput) (*mcp.CallToolResult, RemoveGuardrailOutput, error) {
|
||||||
|
if err := t.store.RemoveGuardrail(ctx, in.ID); err != nil {
|
||||||
|
return nil, RemoveGuardrailOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, RemoveGuardrailOutput{Removed: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// list_guardrails
|
||||||
|
|
||||||
|
type ListGuardrailsInput struct {
|
||||||
|
Tag string `json:"tag,omitempty" jsonschema:"filter by tag"`
|
||||||
|
Severity string `json:"severity,omitempty" jsonschema:"filter by severity: low, medium, high, or critical"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListGuardrailsOutput struct {
|
||||||
|
Guardrails []ext.AgentGuardrail `json:"guardrails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) ListGuardrails(ctx context.Context, _ *mcp.CallToolRequest, in ListGuardrailsInput) (*mcp.CallToolResult, ListGuardrailsOutput, error) {
|
||||||
|
guardrails, err := t.store.ListGuardrails(ctx, in.Tag, in.Severity)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListGuardrailsOutput{}, err
|
||||||
|
}
|
||||||
|
if guardrails == nil {
|
||||||
|
guardrails = []ext.AgentGuardrail{}
|
||||||
|
}
|
||||||
|
return nil, ListGuardrailsOutput{Guardrails: guardrails}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// add_project_skill
|
||||||
|
|
||||||
|
type AddProjectSkillInput struct {
|
||||||
|
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
||||||
|
SkillID uuid.UUID `json:"skill_id" jsonschema:"skill id to link"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddProjectSkillOutput struct {
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
SkillID uuid.UUID `json:"skill_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) AddProjectSkill(ctx context.Context, req *mcp.CallToolRequest, in AddProjectSkillInput) (*mcp.CallToolResult, AddProjectSkillOutput, error) {
|
||||||
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, AddProjectSkillOutput{}, err
|
||||||
|
}
|
||||||
|
if err := t.store.AddProjectSkill(ctx, project.ID, in.SkillID); err != nil {
|
||||||
|
return nil, AddProjectSkillOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, AddProjectSkillOutput{ProjectID: project.ID, SkillID: in.SkillID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove_project_skill
|
||||||
|
|
||||||
|
type RemoveProjectSkillInput struct {
|
||||||
|
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
||||||
|
SkillID uuid.UUID `json:"skill_id" jsonschema:"skill id to unlink"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveProjectSkillOutput struct {
|
||||||
|
Removed bool `json:"removed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) RemoveProjectSkill(ctx context.Context, req *mcp.CallToolRequest, in RemoveProjectSkillInput) (*mcp.CallToolResult, RemoveProjectSkillOutput, error) {
|
||||||
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, RemoveProjectSkillOutput{}, err
|
||||||
|
}
|
||||||
|
if err := t.store.RemoveProjectSkill(ctx, project.ID, in.SkillID); err != nil {
|
||||||
|
return nil, RemoveProjectSkillOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, RemoveProjectSkillOutput{Removed: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// list_project_skills
|
||||||
|
|
||||||
|
type ListProjectSkillsInput struct {
|
||||||
|
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListProjectSkillsOutput struct {
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
Skills []ext.AgentSkill `json:"skills"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) ListProjectSkills(ctx context.Context, req *mcp.CallToolRequest, in ListProjectSkillsInput) (*mcp.CallToolResult, ListProjectSkillsOutput, error) {
|
||||||
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListProjectSkillsOutput{}, err
|
||||||
|
}
|
||||||
|
skills, err := t.store.ListProjectSkills(ctx, project.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListProjectSkillsOutput{}, err
|
||||||
|
}
|
||||||
|
if skills == nil {
|
||||||
|
skills = []ext.AgentSkill{}
|
||||||
|
}
|
||||||
|
return nil, ListProjectSkillsOutput{ProjectID: project.ID, Skills: skills}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// add_project_guardrail
|
||||||
|
|
||||||
|
type AddProjectGuardrailInput struct {
|
||||||
|
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
||||||
|
GuardrailID uuid.UUID `json:"guardrail_id" jsonschema:"guardrail id to link"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddProjectGuardrailOutput struct {
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
GuardrailID uuid.UUID `json:"guardrail_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) AddProjectGuardrail(ctx context.Context, req *mcp.CallToolRequest, in AddProjectGuardrailInput) (*mcp.CallToolResult, AddProjectGuardrailOutput, error) {
|
||||||
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, AddProjectGuardrailOutput{}, err
|
||||||
|
}
|
||||||
|
if err := t.store.AddProjectGuardrail(ctx, project.ID, in.GuardrailID); err != nil {
|
||||||
|
return nil, AddProjectGuardrailOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, AddProjectGuardrailOutput{ProjectID: project.ID, GuardrailID: in.GuardrailID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove_project_guardrail
|
||||||
|
|
||||||
|
type RemoveProjectGuardrailInput struct {
|
||||||
|
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
||||||
|
GuardrailID uuid.UUID `json:"guardrail_id" jsonschema:"guardrail id to unlink"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveProjectGuardrailOutput struct {
|
||||||
|
Removed bool `json:"removed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) RemoveProjectGuardrail(ctx context.Context, req *mcp.CallToolRequest, in RemoveProjectGuardrailInput) (*mcp.CallToolResult, RemoveProjectGuardrailOutput, error) {
|
||||||
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, RemoveProjectGuardrailOutput{}, err
|
||||||
|
}
|
||||||
|
if err := t.store.RemoveProjectGuardrail(ctx, project.ID, in.GuardrailID); err != nil {
|
||||||
|
return nil, RemoveProjectGuardrailOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, RemoveProjectGuardrailOutput{Removed: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// list_project_guardrails
|
||||||
|
|
||||||
|
type ListProjectGuardrailsInput struct {
|
||||||
|
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListProjectGuardrailsOutput struct {
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
Guardrails []ext.AgentGuardrail `json:"guardrails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SkillsTool) ListProjectGuardrails(ctx context.Context, req *mcp.CallToolRequest, in ListProjectGuardrailsInput) (*mcp.CallToolResult, ListProjectGuardrailsOutput, error) {
|
||||||
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListProjectGuardrailsOutput{}, err
|
||||||
|
}
|
||||||
|
guardrails, err := t.store.ListProjectGuardrails(ctx, project.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListProjectGuardrailsOutput{}, err
|
||||||
|
}
|
||||||
|
if guardrails == nil {
|
||||||
|
guardrails = []ext.AgentGuardrail{}
|
||||||
|
}
|
||||||
|
return nil, ListProjectGuardrailsOutput{ProjectID: project.ID, Guardrails: guardrails}, nil
|
||||||
|
}
|
||||||
@@ -209,7 +209,30 @@ type Opportunity struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ContactHistory struct {
|
type ContactHistory struct {
|
||||||
Contact ProfessionalContact `json:"contact"`
|
Contact ProfessionalContact `json:"contact"`
|
||||||
Interactions []ContactInteraction `json:"interactions"`
|
Interactions []ContactInteraction `json:"interactions"`
|
||||||
Opportunities []Opportunity `json:"opportunities"`
|
Opportunities []Opportunity `json:"opportunities"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agent Skills & Guardrails
|
||||||
|
|
||||||
|
type AgentSkill struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentGuardrail struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ Use AMCS as memory with two scopes:
|
|||||||
6. If no strong project match exists, you may use global notebook memory with no project.
|
6. If no strong project match exists, you may use global notebook memory with no project.
|
||||||
7. If multiple projects plausibly match, ask the user before reading or writing project memory.
|
7. If multiple projects plausibly match, ask the user before reading or writing project memory.
|
||||||
|
|
||||||
|
## Project Session Startup
|
||||||
|
|
||||||
|
At the start of every project session, after setting the active project:
|
||||||
|
|
||||||
|
1. Call `list_project_skills` to load any saved agent behaviour instructions for the project.
|
||||||
|
2. Call `list_project_guardrails` to load any saved agent constraints for the project.
|
||||||
|
3. Apply all returned skills and guardrails immediately and for the duration of the session.
|
||||||
|
4. Only generate or define new skills and guardrails if none are returned. If you do create new ones, store them with `add_skill` or `add_guardrail` and link them to the project with `add_project_skill` or `add_project_guardrail` so they persist for future sessions.
|
||||||
|
|
||||||
## Project Memory Rules
|
## Project Memory Rules
|
||||||
|
|
||||||
- Use project memory for code decisions, architecture, TODOs, debugging findings, and context specific to the current repo or workstream.
|
- Use project memory for code decisions, architecture, TODOs, debugging findings, and context specific to the current repo or workstream.
|
||||||
@@ -46,6 +55,14 @@ Use AMCS as memory with two scopes:
|
|||||||
- Do not read a file just to make it storable; store the file directly and read it only when the file contents are needed for reasoning.
|
- Do not read a file just to make it storable; store the file directly and read it only when the file contents are needed for reasoning.
|
||||||
- When saving, choose the narrowest correct scope: project if project-specific, global if not.
|
- When saving, choose the narrowest correct scope: project if project-specific, global if not.
|
||||||
|
|
||||||
|
## Skills and Guardrails
|
||||||
|
|
||||||
|
- **Skills** are reusable agent behaviour instructions (e.g. output formatting rules, reasoning strategies, workflow conventions).
|
||||||
|
- **Guardrails** are agent constraints and safety rules (e.g. never delete without confirmation, do not expose secrets). Each guardrail has a severity: `low`, `medium`, `high`, or `critical`.
|
||||||
|
- Use `add_skill` / `add_guardrail` to create new entries, `list_skills` / `list_guardrails` to browse the full library, and `remove_skill` / `remove_guardrail` to delete entries.
|
||||||
|
- Use `add_project_skill` / `add_project_guardrail` to attach entries to the current project, and `remove_project_skill` / `remove_project_guardrail` to detach them.
|
||||||
|
- Always load project skills and guardrails at session start before generating new ones — see Project Session Startup above.
|
||||||
|
|
||||||
## Short Operational Form
|
## Short Operational Form
|
||||||
|
|
||||||
Use AMCS memory in project scope when the current work matches a known project. If no clear project matches, global notebook memory is allowed for non-project-specific information. Store durable notes with `capture_thought`, store supporting binary artifacts with `save_file`, prefer saving a file directly when the artifact itself is what matters, browse stored files with `list_files`, and load them with `load_file` only when their contents are needed. Never store project-specific memory globally when a matching project exists, and never store memory in the wrong project. If project matching is ambiguous, ask the user.
|
Use AMCS memory in project scope when the current work matches a known project. If no clear project matches, global notebook memory is allowed for non-project-specific information. At the start of every project session call `list_project_skills` and `list_project_guardrails` and apply what is returned; only create new skills or guardrails if none exist. Store durable notes with `capture_thought`, store supporting binary artifacts with `save_file`, prefer saving a file directly when the artifact itself is what matters, browse stored files with `list_files`, and load them with `load_file` only when their contents are needed. Never store project-specific memory globally when a matching project exists, and never store memory in the wrong project. If project matching is ambiguous, ask the user.
|
||||||
|
|||||||
39
migrations/017_agent_skills_guardrails.sql
Normal file
39
migrations/017_agent_skills_guardrails.sql
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
create table if not exists agent_skills (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
name text not null,
|
||||||
|
description text not null default '',
|
||||||
|
content text not null,
|
||||||
|
tags text[] not null default '{}',
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint agent_skills_name_unique unique (name)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists agent_guardrails (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
name text not null,
|
||||||
|
description text not null default '',
|
||||||
|
content text not null,
|
||||||
|
severity text not null default 'medium' check (severity in ('low', 'medium', 'high', 'critical')),
|
||||||
|
tags text[] not null default '{}',
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint agent_guardrails_name_unique unique (name)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists project_skills (
|
||||||
|
project_id uuid not null references projects(guid) on delete cascade,
|
||||||
|
skill_id uuid not null references agent_skills(id) on delete cascade,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
primary key (project_id, skill_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists project_guardrails (
|
||||||
|
project_id uuid not null references projects(guid) on delete cascade,
|
||||||
|
guardrail_id uuid not null references agent_guardrails(id) on delete cascade,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
primary key (project_id, guardrail_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists project_skills_project_id_idx on project_skills (project_id);
|
||||||
|
create index if not exists project_guardrails_project_id_idx on project_guardrails (project_id);
|
||||||
@@ -28,4 +28,11 @@ GRANT ALL ON TABLE public.professional_contacts TO amcs;
|
|||||||
GRANT ALL ON TABLE public.contact_interactions TO amcs;
|
GRANT ALL ON TABLE public.contact_interactions TO amcs;
|
||||||
GRANT ALL ON TABLE public.opportunities TO amcs;
|
GRANT ALL ON TABLE public.opportunities TO amcs;
|
||||||
|
|
||||||
|
GRANT ALL ON TABLE public.stored_files TO amcs;
|
||||||
|
GRANT ALL ON TABLE public.agent_guardrails TO amcs;
|
||||||
|
GRANT ALL ON TABLE public.agent_skills TO amcs;
|
||||||
|
|
||||||
|
GRANT ALL ON TABLE public.project_skills TO amcs;
|
||||||
|
|
||||||
|
GRANT ALL ON TABLE public.project_guardrails TO amcs;
|
||||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO amcs;
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO amcs;
|
||||||
Reference in New Issue
Block a user