Files
Evan 102b506b0b fix(agents,hands): per-agent/per-hand mcp_servers / skills allowlists (#87) (#92)
All 32 agent manifests and 17 hands shipped with empty mcp_servers /
skills lists, which the kernel interprets as "no filter" — every
globally-configured MCP server's tools and every installed skill get
injected into the prompt on every LLM call. On a typical instance (9
MCP servers, ~85 MCP tools + ~82 built-in tools) that's ~50k input
tokens per turn spent on definitions the agent never uses.

Changes
-------

32 agents/*/agent.toml:
  - mcp_servers: 1-4 per agent. memory wherever state persists across
    turns; fetch / exa-search / brave-search only where the prompt
    actually calls for web; git / github / filesystem on engineering
    agents; gmail / google-calendar / linear / jira on productivity
    agents whose prompts mention them.
  - skills: per-role allowlist driven by what the system_prompt names
    (e.g. coder → rust/python/typescript/git/shell-scripting; devops-
    lead → docker/kubernetes/terraform/ansible/ci-cd/helm/prometheus/
    sysadmin). Generalists (assistant) keep skills = [] (see "Open
    items" below).
  - skills_disabled = true on the four short-conversational agents
    (hello-world, recipe-assistant, health-tracker, home-automation).
    Their system prompts never instruct the LLM to consult any skill,
    so loading all 60 was pure waste. They also drop the explicit
    max_history_messages override and inherit the kernel default (60).
  - max_history_messages tiered by workload shape:
      60  short conversational (hello-world, recipe, health-tracker,
          home-automation) — inherits the rising kernel default
          (`DEFAULT_MAX_HISTORY_MESSAGES = 60`); no override needed.
      60  single-turn task agents (writer, translator, doc-writer,
          email-assistant, customer-support, sales-assistant, recruit-
          er, social-media, personal-finance, tutor, travel-planner,
          meeting-assistant, ops, devops-lead, planner) — explicit
          override at the same value to lock the cap if the kernel
          default moves again.
      80  multi-step / tool-heavy (coder, debugger, architect, code-
          reviewer, test-engineer, security-auditor, analyst, data-
          scientist, academic-researcher, researcher, legal-assistant)
      120 coordinators (assistant, orchestrator) — long multi-agent
          sessions where prompt-cache continuity is critical
    All values sit at or above the kernel default. Pinning lower
    would thrash the prompt cache (the failure mode #91 fixed for
    the creator hand by *raising* the cap, not lowering it).

17 hands/*/HAND.toml:
  - hand-level mcp_servers / skills now declared on every hand, so
    every [agents.*] inside inherits a sensible allowlist.
  - skills_disabled = true placed on each [agents.*] inside clip and
    creator (pure media pipelines that don't benefit from any skill).
    HandDefinitionRaw in librefang-hands does NOT have a top-level
    skills_disabled field — declaring it at the hand top level would
    be silently dropped by serde, so the setting must live on the
    AgentManifest of each sub-agent role.
  - devteam: expand existing mcp_servers = ["github"] to include
    memory / git / filesystem; populate skills with the expected
    dev-team expertise (replacing the placeholder skills = []).
  - wiki: replace placeholder mcp_servers = [] with [memory, fetch,
    filesystem]. Hand-level skills stays [].
  - lead: hand-level skills was originally [email-writer, writing-
    coach, interview-prep]; interview-prep is for job-interview
    preparation, not lead generation. Replaced with data-analyst
    (used by the qualification-scoring step in the prompt).

schema.toml: register mcp_servers / skills / max_history_messages on
the agent field schema so machine consumers (RegistrySchema in
librefang-types) see the new top-level fields. The
max_history_messages description now points at
librefang_runtime::agent_loop::DEFAULT_MAX_HISTORY_MESSAGES (60
today) by name, so the schema doesn't go stale when the constant
moves again.

agents/README.md: example block + "Adding a New Agent" checklist
mention the allowlists; max_history_messages example is shown
commented out with a prompt-cache caveat.

Open items
----------

`assistant` (the default user-facing agent) keeps `skills = []`
deliberately. It is the generalist entry point — capping its skill
surface at a small allowlist would defeat its "delegate to any
specialist" job. The trade-off is that this single agent still pays
the full skill-definition load on every turn; operators who want a
strict allowlist for `assistant` can override it after install.

Why not adopt PR #89's approach
-------------------------------

#89 covers similar ground but with three issues this PR avoids:

1. mcp_servers = ["_none"] sentinel. #89's body explicitly notes
   it's pending upstream librefang#4808 (mcp_disabled). Shipping a
   magic-string today means coming back later to clean it up. This
   PR uses real allowlists.
2. max_history_messages = 8 / 12 / 15 / 20. Far below today's
   kernel default (60) and #91's direction for long-workflow hands
   (80–120). Every turn that hits the cap invalidates the cached
   prompt prefix; the cost of cache misses exceeds the saving from
   shorter history. This PR uses 60–120.
3. Doubling max_llm_tokens_per_hour (coder 200k→500k, assistant
   300k→500k) widens the per-agent budget — the opposite direction
   from #87's "reduce per-call cost" goal. Left to the operator's
   instance-specific tuning.

Refs librefang/librefang-registry#87, librefang/librefang-registry#89
2026-05-12 09:30:21 +09:00

960 lines
28 KiB
TOML

# LibreFang Registry Schema
# =========================
# Machine-parseable schema definition for all registry content types.
# Consumed by the RegistrySchema Rust type (librefang-types).
# ═══════════════════════════════════════════════════════════════════════════════
# PROVIDER / MODEL SCHEMA
# ═══════════════════════════════════════════════════════════════════════════════
[provider]
description = "LLM provider configuration"
file_pattern = "providers/*.toml"
[provider.fields.id]
type = "string"
required = true
description = "Unique provider identifier (lowercase, hyphenated)"
example = "anthropic"
[provider.fields.display_name]
type = "string"
required = true
description = "Human-readable display name"
example = "Anthropic"
[provider.fields.api_key_env]
type = "string"
required = true
description = "Environment variable name for the API key"
example = "PROVIDER_API_KEY"
[provider.fields.base_url]
type = "string"
required = true
description = "Default API base URL"
example = "https://api.example.com"
[provider.fields.key_required]
type = "bool"
required = true
description = "Whether an API key is needed (false for local providers)"
example = true
[provider.fields.api_key]
type = "string"
required = false
description = "API key value (stored securely in secrets.env, not in the TOML file)"
secret = true
# --- Provider: nested [[models]] section ---
[provider.sections.models]
description = "Model entries"
repeatable = true
[provider.sections.models.fields.id]
type = "string"
required = true
description = "Unique model identifier (API model ID)"
example = "claude-sonnet-4-20250514"
[provider.sections.models.fields.display_name]
type = "string"
required = true
description = "Human-readable display name"
example = "Claude Sonnet"
[provider.sections.models.fields.tier]
type = "string"
required = true
description = "Capability tier: frontier (cutting-edge, most capable), smart (cost-effective), balanced (speed/cost), fast (cheapest for simple tasks), local (Ollama, vLLM, LM Studio)"
options = ["frontier", "smart", "balanced", "fast", "local"]
example = "smart"
[provider.sections.models.fields.modality]
type = "string"
required = false
description = "Model modality. 'text' (default) is a chat/LLM; 'image' is an image-generation model; 'audio' is a speech / TTS model; 'video' is a video-generation model; 'music' is a music / lyrics generation model. For non-text modalities, context_window / max_output_tokens are optional and input_cost_per_m / output_cost_per_m may be 0 when the model is billed per call."
options = ["text", "image", "audio", "video", "music"]
default = "text"
example = "text"
[provider.sections.models.fields.context_window]
type = "number"
required = true
description = "Maximum input tokens. Required for modality='text'; optional for image/audio models where no published context gate exists."
example = 128000
[provider.sections.models.fields.max_output_tokens]
type = "number"
required = true
description = "Maximum output tokens. Required for modality='text'; optional for image/audio models."
example = 16384
[provider.sections.models.fields.input_cost_per_m]
type = "number"
required = true
description = "USD per million text input tokens (0.0 for free/local)"
example = 2.5
[provider.sections.models.fields.output_cost_per_m]
type = "number"
required = true
description = "USD per million text output tokens (0.0 for free/local)"
example = 10.0
[provider.sections.models.fields.image_input_cost_per_m]
type = "number"
required = false
description = "USD per million image input tokens (for image/multimodal models)"
example = 8.0
[provider.sections.models.fields.image_output_cost_per_m]
type = "number"
required = false
description = "USD per million image output tokens (for image-generation models)"
example = 30.0
[provider.sections.models.fields.per_call_cost]
type = "number"
required = false
description = "Flat USD cost per generation, for models billed per call rather than per token. Strongly recommended for modality='video' and modality='music' (which have no per-token pricing); may also be used for image/audio models that quote a flat per-image or per-request rate. When absent for a per-call modality, the runtime metering layer records the call as $0 and logs a warning."
example = 0.33
[provider.sections.models.fields.last_verified]
type = "string"
required = false
description = "ISO date when pricing was last verified against official sources"
example = "2025-03-15"
[provider.sections.models.fields.supports_tools]
type = "bool"
required = false
description = "Tool/function calling support"
default = false
[provider.sections.models.fields.supports_vision]
type = "bool"
required = false
description = "Vision/image input support"
default = false
[provider.sections.models.fields.supports_streaming]
type = "bool"
required = false
description = "Streaming response support"
default = true
[provider.sections.models.fields.supports_thinking]
type = "bool"
required = false
description = "Extended thinking / reasoning support"
default = false
[provider.sections.models.fields.reasoning_echo_policy]
type = "string"
required = false
description = "How the OpenAI-compatible driver must handle the reasoning_content field on historical assistant turns when this model is used. 'none' (default) omits the field entirely. 'strip' is required by DeepSeek-R1 / deepseek-reasoner — the API rejects multi-turn requests that carry reasoning_content from previous turns. 'echo' is required by DeepSeek V4 Flash and other thinking-mode-on models — the original thinking text MUST be round-tripped on assistant turns that contain tool_calls, otherwise the API returns 400. 'empty_string' is required by Moonshot / Kimi K2 — the field must be present (empty string) on tool_calls turns, with thinking disabled wire-side."
options = ["none", "strip", "echo", "empty_string"]
default = "none"
[provider.sections.models.fields.aliases]
type = "array"
required = false
description = "Alternative names for this model"
item_type = "string"
example = ["alias1", "alias2"]
# ═══════════════════════════════════════════════════════════════════════════════
# AGENT SCHEMA
# ═══════════════════════════════════════════════════════════════════════════════
[agent]
description = "Agent configuration"
file_pattern = "agents/<name>/agent.toml"
[agent.fields.name]
type = "string"
required = true
description = "Agent identifier, must match directory name"
example = "agent-name"
[agent.fields.version]
type = "string"
required = false
description = "Semver version string"
example = "0.1.0"
[agent.fields.description]
type = "string"
required = true
description = "One-sentence description of what this agent does"
example = "What this agent does"
[agent.fields.author]
type = "string"
required = false
description = "Author or organization"
example = "author-name"
[agent.fields.module]
type = "string"
required = true
description = "Runtime module (builtin:chat, builtin:tool, etc.)"
example = "builtin:chat"
[agent.fields.mcp_servers]
type = "array"
required = false
description = "MCP server allowlist. Empty list (default) means every connected MCP server's tools are injected into this agent's prompt — on a typical instance that is ~85 tool definitions and ~50k tokens per call. Set an explicit list to trim the surface to only the servers this agent actually uses (e.g. [\"memory\", \"git\", \"github\"])."
item_type = "string"
example = ["memory", "git", "github"]
[agent.fields.skills]
type = "array"
required = false
description = "Skill allowlist. Empty list (default) means every installed skill is available. Set an explicit list to load only what this agent needs."
item_type = "string"
example = ["rust-expert", "python-expert"]
[agent.fields.max_history_messages]
type = "number"
required = false
description = "Per-agent override for the message-history trim cap. Omit (default) inherits from KernelConfig.max_history_messages, which itself falls back to the compiled constant librefang_runtime::agent_loop::DEFAULT_MAX_HISTORY_MESSAGES (60 today). Values below 4 are clamped at runtime with a warning. Pinning this *below* the kernel default trashes the prompt cache — every turn that hits the cap invalidates the cached prefix and costs more than the smaller history saves. Set this only to raise the cap for iteration-heavy agents that legitimately need more context, or to lower it for agents whose conversations are demonstrably short and where you've measured the trade-off."
example = 80
# --- Agent: [model] section ---
[agent.sections.model]
description = "Agent's LLM configuration"
repeatable = false
[agent.sections.model.fields.provider]
type = "string"
required = false
description = "Provider ID or \"default\""
example = "default"
[agent.sections.model.fields.model]
type = "string"
required = false
description = "Model ID or \"default\""
example = "default"
[agent.sections.model.fields.max_tokens]
type = "number"
required = false
description = "Max response tokens"
example = 4096
[agent.sections.model.fields.temperature]
type = "number"
required = false
description = "Sampling temperature (0.0-2.0)"
example = 0.7
[agent.sections.model.fields.system_prompt]
type = "string"
required = true
description = "Behavioral instructions for the agent"
# --- Agent: [metadata.routing] section ---
[agent.sections.metadata]
description = "Agent metadata"
repeatable = false
[agent.sections.metadata.sections.routing]
description = "Routing configuration"
repeatable = false
[agent.sections.metadata.sections.routing.fields.aliases]
type = "array"
required = false
description = "Phrases that route directly to this agent"
item_type = "string"
example = ["exact match phrases"]
[agent.sections.metadata.sections.routing.fields.weak_aliases]
type = "array"
required = false
description = "Keywords that suggest this agent"
item_type = "string"
example = ["partial match keywords"]
# --- Agent: [resources] section ---
[agent.sections.resources]
description = "Resource limits"
repeatable = false
[agent.sections.resources.fields.max_llm_tokens_per_hour]
type = "number"
required = false
description = "Token budget per hour"
example = 100000
# --- Agent: [capabilities] section ---
[agent.sections.capabilities]
description = "Agent permissions"
repeatable = false
[agent.sections.capabilities.fields.tools]
type = "array"
required = false
description = "List of allowed tool names"
item_type = "string"
example = ["tool1", "tool2"]
[agent.sections.capabilities.fields.network]
type = "array"
required = false
description = "Network access patterns (\"*\" = unrestricted)"
item_type = "string"
example = ["*"]
[agent.sections.capabilities.fields.memory_read]
type = "array"
required = false
description = "Memory read permissions"
item_type = "string"
example = ["*"]
[agent.sections.capabilities.fields.memory_write]
type = "array"
required = false
description = "Memory write permissions"
item_type = "string"
example = ["self.*"]
[agent.sections.capabilities.fields.agent_spawn]
type = "bool"
required = false
description = "Whether this agent can spawn sub-agents"
default = false
# ═══════════════════════════════════════════════════════════════════════════════
# HAND SCHEMA
# ═══════════════════════════════════════════════════════════════════════════════
[hand]
description = "Hand configuration (a hand is a user-facing capability backed by an agent)"
file_pattern = "hands/<name>/HAND.toml"
[hand.fields.id]
type = "string"
required = true
description = "Unique identifier, must match directory name"
example = "hand-id"
[hand.fields.name]
type = "string"
required = true
description = "Human-readable display name"
example = "Hand Name"
[hand.fields.description]
type = "string"
required = true
description = "One-sentence description of what this hand does"
example = "What this hand does"
[hand.fields.category]
type = "string"
required = true
description = "Hand category"
options = [
"communication",
"content",
"data",
"development",
"devops",
"finance",
"productivity",
"research",
"social",
]
example = "productivity"
[hand.fields.icon]
type = "string"
required = false
description = "Emoji icon"
[hand.fields.tools]
type = "array"
required = true
description = "List of tool names this hand uses"
item_type = "string"
example = ["tool1", "tool2"]
# --- Hand: [routing] section ---
[hand.sections.routing]
description = "Routing configuration"
repeatable = false
[hand.sections.routing.fields.aliases]
type = "array"
required = false
description = "Phrases that activate this hand"
item_type = "string"
example = ["activate phrases"]
[hand.sections.routing.fields.weak_aliases]
type = "array"
required = false
description = "Keyword hints for routing"
item_type = "string"
example = ["keyword hints"]
# --- Hand: [[requires]] section ---
[hand.sections.requires]
description = "External dependencies"
repeatable = true
[hand.sections.requires.fields.key]
type = "string"
required = true
description = "Unique dependency key"
example = "python3"
[hand.sections.requires.fields.label]
type = "string"
required = true
description = "Human-readable label"
example = "Python 3"
[hand.sections.requires.fields.requirement_type]
type = "string"
required = true
description = "Type of requirement"
options = ["binary", "package", "service"]
example = "binary"
[hand.sections.requires.fields.check_value]
type = "string"
required = true
description = "Binary name or check command"
example = "python3"
[hand.sections.requires.fields.optional]
type = "bool"
required = false
description = "Whether the dependency is optional"
default = false
[hand.sections.requires.fields.description]
type = "string"
required = false
description = "Why this dependency is needed"
# --- Hand: [[requires]] -> [install] nested section ---
[hand.sections.requires.sections.install]
description = "Platform-specific install instructions"
repeatable = false
[hand.sections.requires.sections.install.fields.macos]
type = "string"
required = false
description = "macOS install command"
example = "brew install python3"
[hand.sections.requires.sections.install.fields.windows]
type = "string"
required = false
description = "Windows install command"
example = "winget install Python.Python.3.12"
[hand.sections.requires.sections.install.fields.linux_apt]
type = "string"
required = false
description = "Linux (apt) install command"
example = "sudo apt install python3"
[hand.sections.requires.sections.install.fields.manual_url]
type = "string"
required = false
description = "URL for manual installation instructions"
example = "https://..."
# --- Hand: [[settings]] section ---
[hand.sections.settings]
description = "User-configurable settings"
repeatable = true
[hand.sections.settings.fields.key]
type = "string"
required = true
description = "Unique setting key"
example = "setting_key"
[hand.sections.settings.fields.label]
type = "string"
required = true
description = "Human-readable label"
example = "Setting Label"
[hand.sections.settings.fields.description]
type = "string"
required = false
description = "What this setting controls"
[hand.sections.settings.fields.setting_type]
type = "string"
required = true
description = "Setting input type"
options = ["toggle", "select", "text", "number"]
example = "toggle"
[hand.sections.settings.fields.default]
type = "string"
required = false
description = "Default value as string"
example = "true"
# --- Hand: [[settings]] -> [[options]] nested section ---
[hand.sections.settings.sections.options]
description = "Available options for select-type settings"
repeatable = true
[hand.sections.settings.sections.options.fields.value]
type = "string"
required = true
description = "Option value"
example = "option1"
[hand.sections.settings.sections.options.fields.label]
type = "string"
required = true
description = "Option display label"
example = "Option Label"
# --- Hand: [agent] section ---
[hand.sections.agent]
description = "The agent that powers this hand"
repeatable = false
[hand.sections.agent.fields.name]
type = "string"
required = true
description = "Agent name"
example = "hand-agent-name"
[hand.sections.agent.fields.description]
type = "string"
required = false
description = "Agent description"
[hand.sections.agent.fields.module]
type = "string"
required = true
description = "Runtime module"
example = "builtin:chat"
[hand.sections.agent.fields.provider]
type = "string"
required = false
description = "Provider ID"
example = "default"
[hand.sections.agent.fields.model]
type = "string"
required = false
description = "Model ID"
example = "default"
[hand.sections.agent.fields.max_tokens]
type = "number"
required = false
description = "Max response tokens"
example = 16384
[hand.sections.agent.fields.temperature]
type = "number"
required = false
description = "Sampling temperature"
example = 0.3
[hand.sections.agent.fields.max_iterations]
type = "number"
required = false
description = "Maximum tool-use iterations"
example = 60
[hand.sections.agent.fields.system_prompt]
type = "string"
required = true
description = "Detailed behavioral prompt for the agent"
# --- Hand: [dashboard] section ---
[hand.sections.dashboard]
description = "Dashboard metrics configuration"
repeatable = false
# --- Hand: [dashboard] -> [[metrics]] nested section ---
[hand.sections.dashboard.sections.metrics]
description = "Dashboard metric entries"
repeatable = true
[hand.sections.dashboard.sections.metrics.fields.label]
type = "string"
required = true
description = "Metric display name"
example = "Metric Name"
[hand.sections.dashboard.sections.metrics.fields.memory_key]
type = "string"
required = true
description = "Memory key to read the metric value from"
example = "metric_memory_key"
[hand.sections.dashboard.sections.metrics.fields.format]
type = "string"
required = true
description = "Display format for the metric value"
options = ["number", "currency", "percentage"]
example = "number"
# --- Hand: [metadata] section ---
[hand.sections.metadata]
description = "Operational metadata"
repeatable = false
[hand.sections.metadata.fields.frequency]
type = "string"
required = false
description = "How often this hand runs"
options = ["continuous", "on-demand", "scheduled"]
example = "continuous"
[hand.sections.metadata.fields.token_consumption]
type = "string"
required = false
description = "Expected token usage level"
options = ["low", "medium", "high"]
example = "low"
[hand.sections.metadata.fields.default_active]
type = "bool"
required = false
description = "Whether this hand is active by default"
default = true
# ═══════════════════════════════════════════════════════════════════════════════
# INTEGRATION SCHEMA
# ═══════════════════════════════════════════════════════════════════════════════
[integration]
description = "External service integration via MCP server"
file_pattern = "mcp/<name>.toml"
[integration.fields.id]
type = "string"
required = true
description = "Unique identifier, must match filename"
example = "integration-id"
[integration.fields.name]
type = "string"
required = true
description = "Human-readable display name"
example = "Service Name"
[integration.fields.description]
type = "string"
required = false
description = "One-sentence description of what this integration provides"
[integration.fields.category]
type = "string"
required = false
description = "Integration category"
options = [
"devtools",
"communication",
"storage",
"monitoring",
"data",
"productivity",
]
example = "devtools"
[integration.fields.icon]
type = "string"
required = false
description = "Emoji icon"
[integration.fields.tags]
type = "array"
required = false
description = "Searchable tags"
item_type = "string"
example = ["tag1", "tag2"]
[integration.fields.setup_instructions]
type = "string"
required = false
description = "Multi-line setup guide"
# --- Integration: [transport] section ---
[integration.sections.transport]
description = "MCP server transport configuration"
repeatable = false
[integration.sections.transport.fields.type]
type = "string"
required = true
description = "Transport type"
options = ["stdio", "sse"]
example = "stdio"
[integration.sections.transport.fields.command]
type = "string"
required = true
description = "Command to launch the MCP server"
example = "npx"
[integration.sections.transport.fields.args]
type = "array"
required = false
description = "Command arguments"
item_type = "string"
example = ["-y", "@pkg/server"]
# --- Integration: [[required_env]] section ---
[integration.sections.required_env]
description = "Required environment variables"
repeatable = true
[integration.sections.required_env.fields.name]
type = "string"
required = true
description = "Environment variable name"
example = "SERVICE_API_KEY"
[integration.sections.required_env.fields.label]
type = "string"
required = true
description = "Human-readable label"
example = "API Key"
[integration.sections.required_env.fields.help]
type = "string"
required = false
description = "Help text explaining how to get this value"
[integration.sections.required_env.fields.is_secret]
type = "bool"
required = false
description = "Whether this is a secret value"
default = true
[integration.sections.required_env.fields.get_url]
type = "string"
required = false
description = "URL where user can obtain the value"
example = "https://..."
# --- Integration: [oauth] section ---
[integration.sections.oauth]
description = "OAuth configuration"
repeatable = false
[integration.sections.oauth.fields.provider]
type = "string"
required = true
description = "OAuth provider identifier"
example = "github"
[integration.sections.oauth.fields.scopes]
type = "array"
required = false
description = "OAuth scopes to request"
item_type = "string"
example = ["repo", "read:org"]
[integration.sections.oauth.fields.auth_url]
type = "string"
required = false
description = "OAuth authorization URL"
example = "https://..."
[integration.sections.oauth.fields.token_url]
type = "string"
required = false
description = "OAuth token exchange URL"
example = "https://..."
# --- Integration: [health_check] section ---
[integration.sections.health_check]
description = "Health check configuration"
repeatable = false
[integration.sections.health_check.fields.interval_secs]
type = "number"
required = false
description = "Health check interval in seconds"
example = 60
[integration.sections.health_check.fields.unhealthy_threshold]
type = "number"
required = false
description = "Number of consecutive failures before marking unhealthy"
example = 3
# ═══════════════════════════════════════════════════════════════════════════════
# SKILL SCHEMA
# ═══════════════════════════════════════════════════════════════════════════════
[skill]
description = "Reusable skill definition (prompt template or script)"
file_pattern = "skills/<name>/skill.toml"
# --- Skill: [skill] section (metadata) ---
# Note: the top-level "skill" content type contains a [skill] section for metadata.
# In the TOML file, these fields live directly under [skill].
[skill.sections.skill]
description = "Skill metadata"
repeatable = false
[skill.sections.skill.fields.name]
type = "string"
required = true
description = "Skill identifier, must match directory name"
example = "skill-name"
[skill.sections.skill.fields.version]
type = "string"
required = false
description = "Semver version string"
example = "0.1.0"
[skill.sections.skill.fields.description]
type = "string"
required = false
description = "One-sentence description of what this skill does"
[skill.sections.skill.fields.author]
type = "string"
required = false
description = "Author or organization"
example = "author-name"
[skill.sections.skill.fields.tags]
type = "array"
required = false
description = "Searchable tags"
item_type = "string"
example = ["tag1", "tag2"]
# --- Skill: [runtime] section ---
[skill.sections.runtime]
description = "Execution runtime configuration"
repeatable = false
[skill.sections.runtime.fields.type]
type = "string"
required = true
description = "Runtime type"
options = ["promptonly", "python", "node", "shell"]
example = "promptonly"
[skill.sections.runtime.fields.entry]
type = "string"
required = false
description = "Entry point file (required for non-promptonly runtimes)"
example = "main.py"
# --- Skill: [input] section ---
[skill.sections.input]
description = "Input parameter definitions (each field is a parameter with type, description, required)"
repeatable = false
# Note: input fields are dynamic — each key is a parameter name with an object value
# containing type, description, and required. This section uses a convention where
# individual parameters are defined as fields.
# --- Skill: [prompt] section ---
[skill.sections.prompt]
description = "Prompt template (required for promptonly runtime)"
repeatable = false
[skill.sections.prompt.fields.template]
type = "string"
required = true
description = "Prompt template string, supports {{param_name}} placeholders"
example = "Use {{param_name}} in the template"
# ═══════════════════════════════════════════════════════════════════════════════
# PLUGIN SCHEMA
# ═══════════════════════════════════════════════════════════════════════════════
[plugin]
description = "Plugin that extends agent behavior via lifecycle hooks"
file_pattern = "plugins/<name>/plugin.toml"
[plugin.fields.name]
type = "string"
required = true
description = "Plugin identifier, must match directory name"
example = "plugin-name"
[plugin.fields.version]
type = "string"
required = true
description = "Semver version string"
example = "0.1.0"
[plugin.fields.description]
type = "string"
required = true
description = "One-sentence description of what this plugin does"
[plugin.fields.author]
type = "string"
required = false
description = "Author or organization"
example = "author-name"
# --- Plugin: [hooks] section ---
# Hook scripts communicate via stdin/stdout JSON:
# ingest receives: {"type": "ingest", "agent_id": "...", "message": "..."}
# ingest returns: {"type": "ingest_result", "memories": [{"content": "..."}]}
# after_turn receives: {"type": "after_turn", "agent_id": "...", "messages": [...]}
# after_turn returns: {"type": "ok"}
[plugin.sections.hooks]
description = "Hook entry points — scripts invoked at specific lifecycle events"
repeatable = false
[plugin.sections.hooks.fields.ingest]
type = "string"
required = false
description = "Script called when a user message is received"
example = "hooks/ingest.py"
[plugin.sections.hooks.fields.after_turn]
type = "string"
required = false
description = "Script called after each conversation turn"
example = "hooks/after_turn.py"