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:
2026-03-30 23:35:54 +02:00
parent e6f00ce636
commit 3c1ca83dc9
14 changed files with 862 additions and 14 deletions

View File

@@ -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 |
| `reparse_thought_metadata` | Re-extract and normalize metadata for stored thoughts |
| `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
@@ -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.
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:
```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
Run the SQL migrations against a local database with:

View File

@@ -1,8 +1,8 @@
server:
host: "0.0.0.0"
port: 8080
read_timeout: "15s"
write_timeout: "30s"
read_timeout: "10m"
write_timeout: "10m"
idle_timeout: "60s"
allowed_origins:
- "*"

View File

@@ -1,8 +1,8 @@
server:
host: "0.0.0.0"
port: 8080
read_timeout: "15s"
write_timeout: "30s"
read_timeout: "10m"
write_timeout: "10m"
idle_timeout: "60s"
allowed_origins:
- "*"

View File

@@ -1,8 +1,8 @@
server:
host: "0.0.0.0"
port: 8080
read_timeout: "15s"
write_timeout: "30s"
read_timeout: "10m"
write_timeout: "10m"
idle_timeout: "60s"
allowed_origins:
- "*"

View File

@@ -173,6 +173,7 @@ func routes(logger *slog.Logger, cfg *config.Config, db *store.DB, provider ai.P
Calendar: tools.NewCalendarTool(db),
Meals: tools.NewMealsTool(db),
CRM: tools.NewCRMTool(db),
Skills: tools.NewSkillsTool(db, activeProjects),
}
mcpHandler := mcpserver.New(cfg.MCP, toolSet)

View File

@@ -11,7 +11,10 @@ import (
"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 {
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) {
if err := r.ParseMultipartForm(maxUploadBytes); err != nil {
if err := r.ParseMultipartForm(multipartFormMemory); err != nil {
return tools.SaveFileDecodedInput{}, err
}

View File

@@ -50,8 +50,8 @@ func defaultConfig() Config {
Server: ServerConfig{
Host: "0.0.0.0",
Port: 8080,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
ReadTimeout: 10 * time.Minute,
WriteTimeout: 10 * time.Minute,
IdleTimeout: 60 * time.Second,
},
MCP: MCPConfig{

View File

@@ -33,6 +33,7 @@ type ToolSet struct {
Calendar *tools.CalendarTool
Meals *tools.MealsTool
CRM *tools.CRMTool
Skills *tools.SkillsTool
}
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.",
}, 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 server
}, &mcp.StreamableHTTPOptions{

294
internal/store/skills.go Normal file
View 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
View 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
}

View File

@@ -213,3 +213,26 @@ type ContactHistory struct {
Interactions []ContactInteraction `json:"interactions"`
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"`
}

View File

@@ -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.
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
- 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.
- 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
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.

View 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);

View File

@@ -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.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;