Merge branch 'skills'

2026-06-05 11:49:42 -06:00
13 changed files with 489 additions and 98 deletions
+8 -2
@@ -736,14 +736,20 @@ available; only the auto-injected prompt text is suppressed.
Coyote comes packaged with some useful built-in agents:
* `coder`: An agent to assist you with all your coding tasks
* `code-reviewer`: A [CodeRabbit](https://coderabbit.ai)-style code reviewer that spawns per-file reviewers using the teammate messaging pattern
* `code-reviewer`: A [CodeRabbit](https://coderabbit.ai)-style code reviewer that spawns per-file reviewers using the
teammate messaging pattern
* `demo`: An example agent to use for reference when learning to create your own agents
* `deep-research`: A graph-based agent designed to perform deep web research
* `explore`: An agent designed to help you explore and understand your codebase
* `file-reviewer`: An agent designed to perform code-review on a single file (used by the `code-reviewer` agent)
* `librarian`: A graph-based agent that researches external references. It finds official docs, production OSS examples,
and web best practices. The "external grep" sibling of `explore` (which handles internal/codebase grep). Designed to
be delegated to by `sisyphus` whenever an unfamiliar library, API, or framework is involved.
* `oracle`: An agent for high-level architecture, design decisions, and complex debugging
* `report-writer`: An agent to polish research findings into clear, citation-preserving final reports
* `sisyphus`: A powerhouse orchestrator agent for writing complex code and acting as a natural language interface for your codebase (similar to ClaudeCode, Gemini CLI, Codex, or OpenCode). Uses sub-agent spawning to delegate to `explore`, `coder`, and `oracle`.
* `sisyphus`: A powerhouse orchestrator agent for writing complex code and acting as a natural language interface for
your codebase (similar to ClaudeCode, Gemini CLI, Codex, or OpenCode). Uses sub-agent spawning to delegate to
`explore`, `librarian`, `coder`, and `oracle`.
* `sql`: A universal SQL agent that enables you to talk to any relational database in natural language
Coyote writes these built-in agents to your agents directory on first run and never overwrites them afterward, so any
+52
@@ -284,6 +284,58 @@ $ ./get_current_time.sh
Fri Oct 24 05:55:04 PM MDT 2025
```
# Reading argument values from `LLM_TOOL_RAW_JSON`
Coyote dispatches a bash tool call by converting the LLM's JSON arguments into shell `--option=<value>` flags via `jq`,
then `eval`-ing the result. The flag values reach your `main` function as `argc_*` variables. For short, single-line
values this works fine.
However, for **large multi-line values, or values dense with shell-significant characters** (markdown table pipes (`|`),
single quotes, em-dashes, etc.), the shell-quoting round-trip can occasionally drop characters or truncate the value
before it reaches your `argc_*` variable. Symptoms include `argc_*` being shorter than what the LLM sent, or starting
mid-content.
To sidestep the shell-quoting layer entirely, read the value directly from the raw JSON envelope that Coyote exports as
the `LLM_TOOL_RAW_JSON` environment variable:
```bash
# shellcheck disable=SC2154
main() {
argc_contents="$(jq -r '.contents' <<< "$LLM_TOOL_RAW_JSON")"
argc_path="$(jq -r '.path' <<< "$LLM_TOOL_RAW_JSON")"
# ... rest of your tool logic using $argc_contents and $argc_path
}
```
The `jq -r` ("raw") flag preserves every byte of the original LLM-sent value, including newlines, quotes, em-dashes,
and shell-special characters, without any shell-quoting layer in between. This is the pattern Coyote's bundled
`fs_write`, `fs_patch`, `execute_command`, `execute_sql_code`, and `send_mail` tools use for their large-payload
options. The argc `# @option --foo!` directives stay in your script so Coyote can build the JSON schema for the LLM
and validate the call, but your `main()` reads from `LLM_TOOL_RAW_JSON` instead of trusting argc's value capture.
## When to use this
- Your option's value can legitimately be many KB of text (file contents, code, email bodies, SQL queries).
- Your option's value can contain shell-significant characters in dense patterns (pipes, single quotes, ANSI escapes).
- You observe that `argc_<option>` is shorter than what the LLM sent, or has corruption near the beginning/middle of the
value.
If your tool only takes short string values (paths, IDs, search queries), you don't need the bypass. The standard argc
flow handles those reliably.
## For agent-local tools
If you're writing tools inside an agent's `tools.sh` (under `<config_dir>/agents/<agent>/tools.sh`), the same value is
exposed as `LLM_AGENT_RAW_JSON` (the raw JSON for the agent function call). The semantics are identical; only the
variable name differs:
```bash
argc_some_field="$(jq -r '.some_field' <<< "$LLM_AGENT_RAW_JSON")"
```
---
# Prompt Helpers
It's often useful to create interactive prompts for our bash tools so that our tools can get input from
users.
+63 -6
@@ -27,16 +27,73 @@ to enable it globally. See the [Tools](Tools#enablingdisabling-global-tools) doc
## Environment Variables
All tools have access to the following environment variables that provide context about the current execution environment:
| Variable | Description |
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| `LLM_OUTPUT` | Indicates where the output of the tool should go. <br>In certain situations, this may be set to a temporary file instead of `/dev/stdout`. |
| `LLM_ROOT_DIR` | The root `config_dir` directory for Coyote <br>(i.e. `dirname $(coyote --info \| grep config_file \| awk '{print $2}')`) |
| `LLM_TOOL_NAME` | The name of the tool being executed |
| `LLM_TOOL_CACHE_DIR` | A directory specific to the tool for storing cache or temporary files |
| Variable | Description |
|----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `LLM_OUTPUT` | Indicates where the output of the tool should go. <br>In certain situations, this may be set to a temporary file instead of `/dev/stdout`. |
| `LLM_ROOT_DIR` | The root `config_dir` directory for Coyote <br>(i.e. `dirname $(coyote --info \| grep config_file \| awk '{print $2}')`) |
| `LLM_TOOL_NAME` | The name of the tool being executed |
| `LLM_TOOL_CACHE_DIR` | A directory specific to the tool for storing cache or temporary files |
| `LLM_TOOL_RAW_JSON` | The raw JSON envelope the LLM sent for this tool call, exactly as received. See [Reading values via LLM_TOOL_RAW_JSON](#reading-values-via-llm_tool_raw_json) below. |
Coyote also searches the tools directory on startup for a `.env` file. If found, all tools in `functions/tools/` will have
the environment variables defined in the `.env` file available to them.
## Reading values via `LLM_TOOL_RAW_JSON`
Coyote exports the raw JSON envelope it received from the LLM as the `LLM_TOOL_RAW_JSON` environment variable on every
tool invocation. Tools can use this to read option values directly from the JSON rather than going through the
`argc_*` variables.
### When to use it
**Bash tools**: This is the recommended pattern for any option that may carry large multi-line content, code, file
contents, or values dense with shell-significant characters (markdown table pipes, single quotes, em-dashes, etc.).
Coyote's bash dispatcher converts JSON to shell `--option=<value>` flags via `jq` and `eval`-s the result; for large or
special-character values, that shell-quoting round-trip can occasionally drop characters or misalign content before it
reaches `argc_*`. Reading from `LLM_TOOL_RAW_JSON` bypasses the shell layer entirely.
```bash
main() {
argc_contents="$(jq -r '.contents' <<< "$LLM_TOOL_RAW_JSON")"
argc_path="$(jq -r '.path' <<< "$LLM_TOOL_RAW_JSON")"
# ... rest of your tool logic using $argc_contents and $argc_path
}
```
This is the pattern Coyote's bundled `fs_write`, `fs_patch`, `execute_command`, `execute_sql_code`, and `send_mail` tools
use for their large-payload options. The argc `# @option --foo!` directives stay in your script so Coyote can build the
JSON schema for the LLM, but your `main()` reads from `LLM_TOOL_RAW_JSON` instead of trusting argc's value capture.
**Python and TypeScript tools**: Coyote's Python and TypeScript dispatchers parse the JSON envelope natively (`json.loads`
/ `JSON.parse`) and pass values directly to your `run()` function as native types. They don't go through shell quoting,
so the `LLM_*_RAW_JSON` escape hatch that bash tools need doesn't affect them. Declared parameters arrive in your
function correctly without needing `LLM_TOOL_RAW_JSON`.
Python and TypeScript tools may still want to read `LLM_TOOL_RAW_JSON` for other reasons:
- Accessing fields the LLM passed that aren't declared in your `run()` signature (telemetry, optional metadata).
- Auditing or logging the original LLM-sent JSON verbatim.
- Debugging when a value isn't what you expected.
```python
# Python: parse the raw JSON when you need beyond-signature access
import json, os
payload = json.loads(os.environ["LLM_TOOL_RAW_JSON"])
extra_field = payload.get("extra_field")
```
```typescript
// TypeScript: parse the raw JSON when you need beyond-signature access
const payload = JSON.parse(process.env.LLM_TOOL_RAW_JSON!);
const extraField = (payload as Record<string, unknown>).extra_field;
```
### Agent-local tools
For tools written under `<config_dir>/agents/<agent>/tools.sh` (or `.py` / `.ts`), the same value is exposed as
`LLM_AGENT_RAW_JSON`, the raw JSON payload for the agent function call. The semantics are identical; only the variable
name differs.
## Custom Bash-Based Tools
To create a Bash-based tool, refer to the [custom bash tools documentation](Custom-Bash-Tools).
+3 -2
@@ -4,8 +4,9 @@ After installation, you can generate the configuration files and directories by
coyote --info
```
Then, you need to set up the Coyote vault by creating a vault password file. Coyote will do this for you automatically and
guide you through the process when you first attempt to access the vault. So, to get started, you can run:
Then, you need to set up the Coyote vault. On your first run, Coyote walks you through choosing a secrets provider
(Local, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, gopass, or 1Password) and configuring it. See the
[vault documentation](Vault) for the full list of providers and configuration details. To get started, you can run:
```sh
coyote --list-secrets
+61
@@ -68,6 +68,10 @@ global_tools: # global tools available to nodes
- web_search_coyote.sh
mcp_servers: # MCP servers available to nodes
- pubmed-search
skills_enabled: true # optional; master switch for skills in `llm` nodes
enabled_skills: # optional; the *universe* of skills referenceable by any llm node
- code-review
- git-master
conversation_starters: # suggested prompts in the UI
- "Research WebAssembly outside of the browser"
variables: # optional; agent variables
@@ -138,6 +142,13 @@ nodes:
write in the same super-step (the validator catches missing reducers at
load time). See [Parallel Execution](#parallel-execution) for the full
reducer reference and merge semantics.
- **`skills_enabled` / `enabled_skills`:** Optional [skills](Skills) policy
for the graph. `skills_enabled: false` turns skills off for every `llm`
node in the graph regardless of node-level settings. `enabled_skills` is
the *universe*: the set of skill names any `llm` node may reference in
its own `enabled_skills`. Subset-violations are caught by the validator at
load time. See [Skills in Graph Agents](Skills#skills-in-graph-agents) for
the full per-node story.
### `{{initial_prompt}}`: Automatically Seeded
@@ -458,6 +469,51 @@ failure message containing one of: `timed out`, `rate limit`, `429`,
other error fails immediately without consuming further attempts. The
default is `1` (no retries).
### Skills on `llm` nodes
`llm` nodes are the only place [skills](Skills) attach inside a graph. Two
optional fields, mirroring the same fields on roles and agents:
```yaml
implement:
type: llm
prompt: "{{plan_summary}}"
tools: [fs_write, fs_patch, fs_cat]
skills_enabled: true # optional; default inherits from graph level
enabled_skills: # optional per-node restriction; must be a subset
- code-review # of graph-level enabled_skills
- verification-gates
```
Semantics (discovery-only, no auto-load):
- **`skills_enabled`:** Per-node override of the skills master switch.
`false` disables skills for this node only: no `skill__list` /
`skill__load` / `skill__unload` meta-tools are exposed to the model.
`true` or omitted inherits the graph-level / agent-level setting.
- **`enabled_skills`:** Per-node restriction of which skills the model can
see and load. The graph-level `enabled_skills` is the universe; the
per-node list must be a subset (the validator catches violations at
load time). When set, `skill__list` shows only these skills and
`skill__load` only accepts these names.
- **The model loads what it needs.** Nothing is auto-loaded. While the
node runs, the model uses `skill__list` to discover what's available
and `skill__load` to bring a skill's body / tools / MCP servers into
scope when it actually needs them. This matches the design intent of
skills as a dynamic capability layer.
- **Per-node policy isolation.** The node temporarily narrows the active
agent's skill policy to the node's `enabled_skills`. The original
policy is restored when the node finishes, so subsequent nodes see
their own policy. Any skills the model loaded during the node persist
into subsequent nodes by default; they're real registry insertions,
not node-scoped state. (Use the skill's own `auto_unload: true`
frontmatter for skills that should clean up automatically at turn
end.)
Agent nodes (which spawn full sub-agents) intentionally have no
`enabled_skills` field. The spawned child agent's own
`config.yaml`/`graph.yaml` declares its skill policy.
---
## rag
@@ -1460,6 +1516,11 @@ A short, honest list of things that bite people:
- [`graph.example.yaml`](https://github.com/Dark-Alex-17/coyote/blob/main/graph.example.yaml) - A fully-commented, full-featured reference
graph agent at the root of the Coyote repository (every top-level field,
every node type).
- Built-in graph agents shipped with Coyote:
[`coder`](https://github.com/Dark-Alex-17/coyote/blob/main/assets/agents/coder) (implement -> verify_build -> verify_tests -> self_review -> fix-loop),
[`deep-research`](https://github.com/Dark-Alex-17/coyote/blob/main/assets/agents/deep-research) (the canonical reference that exercises every node type),
[`librarian`](https://github.com/Dark-Alex-17/coyote/blob/main/assets/agents/librarian) (triage -> parallel doc + OSS search -> synthesize -> trim; a compact illustration of static fan-out with reducers).
See [Agents > Built-In Agents](Agents#built-in-agents) for descriptions.
- [Agents](Agents) - non-graph agent system (config.yaml + LLM loop)
- [Custom Tools](Custom-Tools) - building `tools.sh` / `tools.py` /
`tools.ts` files for use in graph nodes
+10 -4
@@ -170,11 +170,16 @@ The following settings are available in the global configuration for MCP servers
mcp_server_support: true # Enables or disables MCP server support (globally).
mapping_mcp_servers: # Alias for an MCP server or set of servers
git: github,gitmcp
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack')
enabled_mcp_servers: null # Which MCP servers to enable by default.
# Accepts either a YAML list or a comma-separated string. Examples:
# enabled_mcp_servers: github,slack
# enabled_mcp_servers:
# - github
# - slack
```
A special note about `enabled_mcp_servers`: a user can set this to `all` to enable all configured MCP servers in the
`functions/mcp.json` configuration.
A special note about `enabled_mcp_servers`: a user can set this to `all` (or include `all` in the list) to enable all
configured MCP servers in the `functions/mcp.json` configuration.
(See the [Configuration Example](https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml) file for an example global configuration with all options.)
@@ -187,7 +192,8 @@ When running in REPL-mode, the `mcp_server_support` and `enabled_mcp_servers` se
When you create a role, you have the following MCP-related configuration options available to you:
```yaml
enabled_mcp_servers: github # Which MCP servers the role uses.
enabled_mcp_servers: # Which MCP servers the role uses. Accepts either a YAML list (as shown)
- github # or a comma-separated string (e.g. `enabled_mcp_servers: github,slack`).
```
The values for `mapping_mcp_servers` are inherited from the `[global configuration](#global-configuration)`.
+24 -19
@@ -78,6 +78,9 @@ cannot be persisted to a file and saved.
Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation. Multiple skills can be
loaded at once; their instructions stack and their tools/MCP servers union with the active role/agent/session.
> **Requires function calling.** Skills depend on Coyote's function calling system. If `function_calling_support: false`
> in your global config, the `.skill load` and `.skill unload` commands refuse, and the model cannot load skills itself.
| Command | Description |
|------------------------|---------------------------------------------------------------------------------------------------|
| `.skill loaded` | List currently-loaded skills in this session |
@@ -206,25 +209,27 @@ file.
The following settings can be adjusted at runtime:
| Setting | Type | Description |
|----------------------------|---------|--------------------------------------------------------------------------|
| `auto_continue` | boolean | Enable/disable the [Todo System](TODO-System) auto-continuation |
| `max_auto_continues` | integer | Maximum number of automatic continuations |
| `inject_todo_instructions` | boolean | Inject default todo instructions into the system prompt |
| `continuation_prompt` | string | Custom continuation prompt (supports multi-word values; `null` to reset) |
| `temperature` | float | Model temperature parameter |
| `top_p` | float | Model top-p parameter |
| `enabled_tools` | string | Comma-separated list of enabled tools |
| `enabled_mcp_servers` | string | Comma-separated list of enabled MCP servers |
| `save_session` | boolean | Whether to auto-save sessions |
| `compression_threshold` | integer | Token threshold for session compression |
| `max_output_tokens` | integer | Maximum output tokens for the current model |
| `dry_run` | boolean | Enable/disable dry run mode |
| `function_calling_support` | boolean | Enable/disable function calling |
| `mcp_server_support` | boolean | Enable/disable MCP server support |
| `stream` | boolean | Enable/disable streaming |
| `save` | boolean | Enable/disable saving responses |
| `highlight` | boolean | Enable/disable syntax highlighting |
| Setting | Type | Description |
|----------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `auto_continue` | boolean | Enable/disable the [Todo System](TODO-System) auto-continuation |
| `max_auto_continues` | integer | Maximum number of automatic continuations |
| `inject_todo_instructions` | boolean | Inject default todo instructions into the system prompt |
| `continuation_prompt` | string | Custom continuation prompt (supports multi-word values; `null` to reset) |
| `temperature` | float | Model temperature parameter |
| `top_p` | float | Model top-p parameter |
| `enabled_tools` | string | Comma-separated list of enabled tools (e.g. `fs_ls,fs_cat` or `all`); the saved YAML config also accepts a list form |
| `enabled_mcp_servers` | string | Comma-separated list of enabled MCP servers (e.g. `github,slack` or `all`); the saved YAML config also accepts a list form |
| `enabled_skills` | string | Comma-separated list of enabled [skills](Skills) (e.g. `git-master,ai-slop-remover`); `null` clears the override |
| `save_session` | boolean | Whether to auto-save sessions |
| `compression_threshold` | integer | Token threshold for session compression |
| `max_output_tokens` | integer | Maximum output tokens for the current model |
| `dry_run` | boolean | Enable/disable dry run mode |
| `function_calling_support` | boolean | Enable/disable function calling |
| `mcp_server_support` | boolean | Enable/disable MCP server support |
| `skills_enabled` | boolean | Master switch for [skills](Skills). Accepts `true`, `false`, or `null` (inside a session, `null` clears the session-level override; otherwise resets to the default `true`) |
| `stream` | boolean | Enable/disable streaming |
| `save` | boolean | Enable/disable saving responses |
| `highlight` | boolean | Enable/disable syntax highlighting |
![set](./images/repl/set.gif)
+4 -2
@@ -56,8 +56,10 @@ The following table lists the available configuration settings and their default
| `model` | Default configured model or currently in-use model (REPL mode) | The preferred model to use with this role |
| `temperature` | Default `temperature` for the preferred model | Controls the creativity and randomness of the model's responses |
| `top_p` | Default `top_p` for the preferred model | Alternative way to control the model's output diversity, affecting the <br>probability distribution of tokens |
| `enabled_tools` | Global setting for `enabled_tools` | The tools that this role utilizes |
| `enabled_mcp_servers` | Global setting for `enabled_mcp_servers` | The MCP servers that this role utilizes |
| `enabled_tools` | Global setting for `enabled_tools` | The tools that this role utilizes. Accepts either a YAML list or a comma-separated string |
| `enabled_mcp_servers` | Global setting for `enabled_mcp_servers` | The MCP servers that this role utilizes. Accepts either a YAML list or a comma-separated string |
| `skills_enabled` | Global setting for `skills_enabled` | Master switch for [skills](Skills) under this role. Set to `false` to hide all skills |
| `enabled_skills` | Global setting for `enabled_skills` | The [skills](Skills) this role activates. Accepts either a YAML list or a comma-separated string |
| `auto_continue` | Global setting for `auto_continue` | Enable the [Todo System](TODO-System) auto-continuation for this role |
| `max_auto_continues` | Global setting for `max_auto_continues` | Maximum number of automatic continuations before stopping |
| `inject_todo_instructions` | Global setting for `inject_todo_instructions` | Inject default todo tool usage instructions into the system prompt |
+8 -6
@@ -117,11 +117,13 @@ The following settings are available to customize the default behavior of sessio
In addition to the global settings above, individual sessions can override the following settings. These can be set
at runtime using the `.set` command or configured in the session's YAML file:
| Setting | Default | Description |
|----------------------------|-----------------------------------------|---------------------------------------------------------------------------------------------------------------|
| `auto_continue` | Global `auto_continue` value | Enable the [Todo System](TODO-System) auto-continuation for this session. Overrides global and role settings. |
| `max_auto_continues` | Global `max_auto_continues` value | Maximum number of automatic continuations before stopping. Overrides global and role settings. |
| `inject_todo_instructions` | Global `inject_todo_instructions` value | Inject default todo tool usage instructions into the system prompt. Overrides global and role settings. |
| `continuation_prompt` | Global `continuation_prompt` value | Custom prompt used when auto-continuing. Overrides global and role settings. |
| Setting | Default | Description |
|----------------------------|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
| `auto_continue` | Global `auto_continue` value | Enable the [Todo System](TODO-System) auto-continuation for this session. Overrides global and role settings. |
| `max_auto_continues` | Global `max_auto_continues` value | Maximum number of automatic continuations before stopping. Overrides global and role settings. |
| `inject_todo_instructions` | Global `inject_todo_instructions` value | Inject default todo tool usage instructions into the system prompt. Overrides global and role settings. |
| `continuation_prompt` | Global `continuation_prompt` value | Custom prompt used when auto-continuing. Overrides global and role settings. |
| `skills_enabled` | Global `skills_enabled` value | Master switch for [skills](Skills) in this session. `.set skills_enabled null` clears the session override. |
| `enabled_skills` | Inherited via the skill cascade | The active [skills](Skills) for this session. Accepts either a YAML list or a comma-separated string in the saved session YAML. |
For more information on the Todo System, see the [Todo System documentation](TODO-System).
+4 -3
@@ -197,7 +197,8 @@ MCP server entries (and other config files) can reference vault secrets with `{{
install completes, Coyote scans the resulting `mcp.json` for placeholders that are not yet in your vault and either:
- **In a TTY:** prompts you, one secret at a time, whether to add it to the vault now. On the first "Yes", Coyote
initializes the vault password file (if needed). On "No", the secret is deferred and reported at the end.
initializes the vault (if needed; for the Local provider, this just means creating the password file). On "No", the
secret is deferred and reported at the end.
- **In a non-TTY environment:** skips prompts entirely; lists every missing secret in a final reminder block, with the
commands you can run later (`coyote --add-secret <NAME>` or `.vault add <NAME>`).
@@ -249,8 +250,8 @@ asset type plus a `README` describing the customization workflow.
The following are **intentionally** outside the install-remote feature's scope:
- **`config.yaml`** (the global Coyote config). It holds user-specific things like editor preference, vault password file
path, client API keys, OAuth tokens, and similar. Merging a shared `config.yaml` would risk breaking auth or routing
- **`config.yaml`** (the global Coyote config). It holds user-specific things like editor preference, secrets provider
configuration, client API keys, OAuth tokens, and similar. Merging a shared `config.yaml` would risk breaking auth or routing
traffic somewhere unexpected. Settings that genuinely benefit from sharing (like default models) already belong
inside individual agents' or roles' configs.
- **Sessions, RAGs, agent runtime data.** These accumulate as you use Coyote and aren't shareable in a meaningful way.
+131 -24
@@ -11,6 +11,13 @@ Common uses:
- **Toolkit unlocks** that grant a small bundle of tools or MCP servers without changing the active role.
- **One-shot helpers** that auto-unload after the model finishes a task (see [auto_unload](#auto-unload)).
> **Prerequisite: function calling must be enabled.** Skills are built on top of Coyote's function calling system so as
> to not overwhelm the context with skill descriptions. `skill__list`, `skill__load`, and `skill__unload` meta-tools are
> themselves function calls, and most skills grant tools the model needs to invoke. If your global config has
> `function_calling_support: false`, the entire skills system is disabled: the meta-tools are not registered, the REPL
> `.skill load` command refuses, and `coyote --skill <name>` exits with a clear error. See [Function Calling](Tools) to
> enable it.
---
# Skill Definition
@@ -61,12 +68,12 @@ To see complete examples, look at the [bundled built-in skills](https://github.c
The YAML frontmatter at the top of `SKILL.md` is where you declare the skill's metadata and what extra capabilities it
grants when loaded. All fields are optional.
| Field | Default | Description |
|-----------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `description` | empty | Short one-line description shown to the model when it lists available skills. Make it specific: this is what helps the model decide when to load. |
| `enabled_tools` | none | Comma-separated tool names that become available while the skill is loaded. Union with the active role/agent/session's tools. Tools must exist in [visible tools](Tools). |
| `enabled_mcp_servers` | none | Comma-separated MCP server names. Skills can reference servers from your `mcp.json`; those servers are auto-acquired on load and released on unload via reference counting. |
| `auto_unload` | `false` | If `true`, the skill is automatically removed from the registry at the end of every turn where the model produced a final response (no more tool calls). |
| Field | Default | Description |
|-----------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `description` | empty | Short one-line description shown to the model when it lists available skills. Make it specific: this is what helps the model decide when to load. |
| `enabled_tools` | none | Comma-separated tool names that become available while the skill is loaded. Union with the active role/agent/session's tools. Tools must exist in [visible tools](Tools). |
| `enabled_mcp_servers` | none | MCP server names the skill needs. Accepts a YAML list (preferred) or a comma-separated string. Skills can reference servers from your `mcp.json`; those servers are auto-acquired on load and released on unload via reference counting. |
| `auto_unload` | `false` | If `true`, the skill is automatically removed from the registry at the end of every turn where the model produced a final response (no more tool calls). |
## Body
@@ -99,16 +106,18 @@ your current context with git knowledge, frontend conventions, or any other modu
## REPL commands
| Command | Effect |
|---------------------------------------------|--------------------------------------------------------------------------------------------------|
| `.skill loaded` | List currently-loaded skills in this session. |
| `.skill load <name>` | Load a skill. Validates policy + compatibility, then refreshes the tool scope. |
| `.skill unload <name>` | Unload a loaded skill. Releases any MCP servers it pulled in. |
| `.edit skill <name>` | Open an existing skill in `$EDITOR` (fails if the skill doesn't exist). |
| `.skill <name>` | Open the skill in `$EDITOR`. Creates a scaffolded `SKILL.md` if missing. |
| `.delete skill` | Interactive prompt to choose installed skills to delete. |
| `.install skills` | Reinstall all bundled built-in skills, overwriting existing copies after confirmation. |
| `.install remote <git-url> --filter skills` | Install only `skills/` from a remote repo. See [Sharing Configurations](Sharing-Configurations). |
| Command | Effect |
|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `.skill loaded` | List currently-loaded skills in this session. |
| `.skill load <name>` | Load a skill. Validates policy + compatibility, then refreshes the tool scope. |
| `.skill unload <name>` | Unload a loaded skill. Releases any MCP servers it pulled in. |
| `.edit skill <name>` | Open an existing skill in `$EDITOR` (fails if the skill doesn't exist). |
| `.skill <name>` | Open the skill in `$EDITOR`. Creates a scaffolded `SKILL.md` if missing. |
| `.delete skill` | Interactive prompt to choose installed skills to delete. |
| `.install skills` | Reinstall all bundled built-in skills, overwriting existing copies after confirmation. |
| `.install remote <git-url> --filter skills` | Install only `skills/` from a remote repo. See [Sharing Configurations](Sharing-Configurations). |
| `.set skills_enabled <true\|false\|null>` | Flip the master switch at runtime. Inside a session this sets the session override; otherwise it updates the global default (`null` restores the default `true`). |
| `.set enabled_skills <csv\|null>` | Replace the global default-active skill list at runtime, e.g. `.set enabled_skills git-master,ai-slop-remover`. Use `null` to clear. |
## CLI flags
@@ -165,23 +174,119 @@ Example role that opts into a different skill set than the global default:
```markdown
---
name: frontend-dev
enabled_skills: git-master, ai-slop-remover, frontend-ui-ux
enabled_skills:
- git-master
- ai-slop-remover
- frontend-ui-ux
---
You are a frontend specialist.
```
`enabled_skills` accepts either a YAML list (as shown above) or a comma-separated string
(`enabled_skills: git-master,ai-slop-remover,frontend-ui-ux`). Both forms parse to the same value.
When this role is active, the model sees exactly those three skills in `skill__list`. Any other installed skill is
filtered out of the response and cannot be loaded. `skill__load` rejects it with `"Skill 'X' is not enabled in this context"`.
## Validation
Three validation points enforce these rules:
Four validation points enforce these rules:
| Where | What is checked | Severity |
|------------------------|---------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| **Config load** | Every name in any `enabled_skills` exists on disk AND is in global `visible_skills` (when `visible_skills` is set). | **Hard error**. Refuses to start with the offending name + level. |
| **Runtime list/build** | For each enabled skill: declared tools require function calling; declared MCPs require MCP support. | **Filter + warn**. Incompatible skills are dropped from `skill__list`. |
| **`skill__load` call** | Skill is in the effective enabled set AND compatible with current feature flags. | **Tool error to model** with a distinct message per failure mode. |
| Where | What is checked | Severity |
|-------------------------|---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| **Global feature gate** | `function_calling_support: true` is required for skills to exist at all (registry/tools/REPL/CLI). | **Skills are silently disabled**. No `skill__*` tools, `.skill load` and `--skill` refuse. |
| **Config load** | Every name in any `enabled_skills` exists on disk and is in global `visible_skills` (when `visible_skills` is set). | **Hard error**. Refuses to start with the offending name + level. |
| **Runtime list/build** | For each enabled skill: declared MCPs require MCP support. | **Filter + warn**. Incompatible skills are dropped from `skill__list`. |
| **`skill__load` call** | Skill is in the effective enabled set and its declared MCPs are usable. | **Tool error to model** with a distinct message per failure mode. |
> **Skills require function calling.** Skills are useless without it. The meta-tools (`skill__list`, `skill__load`,
> `skill__unload`) are themselves function calls, and most skills grant tools the model needs to invoke. If
> `function_calling_support: false` in your global config, the entire skills feature is off. The REPL `.skill load`
> command, the CLI `--skill <name>` flag, and the model's own `skill__load` invocation all refuse with a clear error.
---
# Skills in Graph Agents
[Graph agents](Graph-Agents) integrate with the skill system at two levels: a graph-wide *universe* set at the top of
`graph.yaml`, and a per-node *auto-load* set on `llm` nodes. Other node types (script, approval, input, rag, map, end)
have no skill surface. Skills only apply where a model call happens.
## Graph level — the universe
The top-level `skills_enabled` and `enabled_skills` fields in `graph.yaml` work just like they do on a normal agent's
`config.yaml`: they declare the policy ceiling for the whole graph.
```yaml
name: coder
skills_enabled: true
enabled_skills:
- code-review
- git-master
- verification-gates
```
- **`skills_enabled: false`** at the graph level turns skills off for every `llm` node in the graph regardless of
node-level fields. Equivalent to "skills don't exist for this graph."
- **`enabled_skills`** at the graph level is the *universe*: the set of skill names any `llm` node in the graph is
allowed to reference in its own `enabled_skills`. The validator rejects per-node entries that aren't in this set at
load time.
- Omitting `enabled_skills` inherits whatever the role / global cascade resolves to (typically "all visible").
## Per-node: discovery-only on `llm` nodes
`llm` nodes can independently declare `skills_enabled` and `enabled_skills`. The graph-level field gates what's
*allowed* in the graph at all; the node-level field narrows that universe to what *this* node's model can see and
load. Nothing is auto-loaded. The model uses `skill__list` and `skill__load` to bring skills in as it needs them,
matching the rest of Coyote's skill model.
```yaml
nodes:
implement:
type: llm
prompt: "{{plan_summary}}"
tools: [fs_write, fs_patch, fs_cat]
enabled_skills: # must be a subset of graph-level enabled_skills
- code-review
- verification-gates
# skills_enabled: false # would disable skills for this node only
```
Semantics:
- **Discovery, not auto-load.** When the node runs, `skill__list` is exposed to the model along with `skill__load`
and `skill__unload`. The model decides which skills (if any) to load based on the task at hand. Loaded skills
compose into the system prompt and union tools/MCP servers via the standard `effective_role` pipeline; the
loading itself goes through the same `refresh_tool_scope` path as `.skill load` in the REPL.
- **Per-node policy.** When `enabled_skills` is set on the node, the graph's effective skill policy is temporarily
narrowed to that subset for the duration of the node. `skill__list` will only show those skills and `skill__load`
will only accept those names. When the node finishes, the original policy is restored so the next node sees its
own.
- **`skills_enabled: false`** at the node level is an off-switch: no `skill__*` meta-tools are exposed and no skills
can be loaded from this node. Useful when one node in an otherwise skill-enabled graph should run with a strict,
narrow context (for example a structured-output extraction step that shouldn't load editorial conventions).
- **Skill state carry-over.** Skills the model loads during a node persist into subsequent nodes (they're real
registry insertions, not node-scoped state). If you want a skill to clean up automatically at turn end, mark it
`auto_unload: true` in the skill's own frontmatter.
## Agent nodes have no skill surface
`agent` nodes spawn a full child agent (graph or normal). That child agent declares its own skill policy in its own
`config.yaml` / `graph.yaml`. Adding a skill field on the agent node would be redundant at best and surprising at
worst. The child author already controls what skills the child uses. So agent nodes intentionally don't accept
`enabled_skills` / `skills_enabled`.
## Validation
| Where | Check | Severity |
|----------------|------------------------------------------------------------------------------------------------------------|------------|
| Graph load | Per-node `enabled_skills` entries are non-empty strings | Hard error |
| Graph load | Per-node `enabled_skills` are a subset of graph-level `enabled_skills` (when set) | Hard error |
| Node execution | Each auto-loaded skill resolves to a real installed skill (otherwise that skill is skipped with a warning) | Warn |
| Node execution | Each auto-loaded skill's MCP / function-calling requirements are satisfied | Warn |
The validator only runs `validate_before_run` (the default); inspecting a graph via `coyote --info -a <name>` does
not trigger validation, so subset-violation errors surface on the first real run.
---
@@ -274,4 +379,6 @@ When skills are enabled, the model has three tools available for managing them i
| `skill__unload` | `name: string` | Unloads a loaded skill. Releases its MCP server handles. |
These are how the model discovers and uses skills mid-conversation. You can disable this discovery channel by setting
`skills_enabled: false` at any level; the three tools then disappear from the model's function list entirely.
`skills_enabled: false` at any level; the three tools then disappear from the model's function list entirely. The same
happens if `function_calling_support: false` is set globally. Without function calling, the skill system has no surface
to operate on, so it is silently disabled across REPL, CLI, agents, and the model's tool list.
+9 -3
@@ -74,7 +74,12 @@ The following settings are available in the global configuration for tools:
function_calling_support: true # Enables or disables function calling in any context
mapping_tools: # Alias for a tool or toolset
fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write'
enabled_tools: null # Which tools to use by default. (e.g. 'fs,web_search_coyote')
enabled_tools: null # Which tools to use by default.
# Accepts either a YAML list or a comma-separated string. Examples:
# enabled_tools: fs,web_search_coyote
# enabled_tools:
# - fs
# - web_search_coyote
visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools')
# - demo_py.py
- execute_command.sh
@@ -113,7 +118,8 @@ context.
When you create a role, you have the following global tool-related configuration options available to you:
```yaml
enabled_tools: query_jira_issues # Which tools the role uses.
enabled_tools: # Which tools the role uses. Accepts a YAML list (as shown)
- web_search_coyote # or a comma-separated string (e.g. `enabled_tools: web_search_coyote`).
```
The values for `mapping_tools` are inherited from the [global configuration](#global-configuration).
@@ -125,7 +131,7 @@ When you create an agent, you have the following global tool-related configurati
```yaml
global_tools: # Which global tools the agent uses
- query_jira_issues.sh
- web_search_coyote.sh
- fs_cat.sh
- fs_ls.sh
```
+112 -27
@@ -1,15 +1,17 @@
The Coyote vault lets users store sensitive secrets and credentials securely so that there's no plaintext secrets
anywhere in your configurations.
anywhere in your configurations.
It's based on the [G-Man library](https://github.com/Dark-Alex-17/gman) (which also comes in a binary format) which
functions as a universal secret management tool.
It's built on the [G-Man library](https://github.com/Dark-Alex-17/gman), which supports multiple secrets providers:
a local encrypted file, AWS Secrets Manager, Google Cloud Secret Manager, Azure Key Vault, gopass, and 1Password. You
pick the one that fits your workflow and Coyote handles the rest.
![Vault Demo](./images/vault/vault-demo.gif)
---
# Usage
The Coyote vault can be used in one of two ways: via the CLI or via the REPL for interactive usage.
The Coyote vault can be used in one of two ways: via the CLI or via the REPL for interactive usage. The same commands
work regardless of which provider you've configured.
## CLI Usage
The vault is utilized from the CLI with the following flags:
@@ -33,6 +35,94 @@ The vault can be accessed from within the Coyote REPL using the `.vault` command
The manipulation of your vault is guided in the same way as the CLI usage, ensuring ease of use.
# Supported Providers
Coyote supports six secrets providers via [G-Man](https://github.com/Dark-Alex-17/gman). The default is **Local** (an encrypted file on this machine), but you
can switch to any of the others.
| Provider | Storage | What it needs |
|-----------------------|---------------------------------------------------------------|---------------------------------------------------------------|
| `local` (default) | Encrypted file at `vault.yml` in your Coyote config directory | A password file you create on first run |
| `aws_secrets_manager` | AWS Secrets Manager | An authenticated AWS CLI (`aws sso login` or `aws configure`) |
| `gcp_secret_manager` | Google Cloud Secret Manager | `gcloud auth application-default login` |
| `azure_key_vault` | Azure Key Vault | `az login` |
| `gopass` | The `gopass` password manager | The `gopass` CLI installed and initialized |
| `one_password` | 1Password | The `op` CLI installed and signed in (`op signin`) |
If you're not logged into the relevant CLI when Coyote needs to read a secret, you'll get an auth error with the
canonical login command. Coyote does not try to log you in automatically.
# Configuration
There are two ways to configure your secrets provider in `config.yaml`:
## Shorthand: `vault_password_file`
If all you want is the default Local provider, just set the path to a password file:
```yaml
vault_password_file: ~/.coyote_password
```
This is shorthand for "use the Local provider with this password file". It's the simplest setup if you don't
want to use a dedicated secrets provider external to Coyote.
## Explicit: `secrets_provider`
For any non-Local provider (or if you want to be explicit about your Local setup), use the `secrets_provider` block:
```yaml
# Local
secrets_provider:
type: local
password_file: ~/.coyote_password
# AWS Secrets Manager
secrets_provider:
type: aws_secrets_manager
aws_profile: default
aws_region: us-east-1
# Google Cloud Secret Manager
secrets_provider:
type: gcp_secret_manager
gcp_project_id: my-project-id
# Azure Key Vault
secrets_provider:
type: azure_key_vault
vault_name: my-vault-name
# gopass
secrets_provider:
type: gopass
store: my-store # Optional; omit to use the default store
# 1Password
secrets_provider:
type: one_password
vault: Production # Optional; omit to use the default vault
account: my.1password.com # Optional; omit to use the default account
```
When `secrets_provider` is set, the legacy `vault_password_file` field is ignored.
> ⚠️ **Important:** The `secrets_provider` block itself cannot use `{{SECRET_NAME}}` interpolation. Coyote needs to
> initialize the vault *before* it can resolve any secrets, so the provider's own configuration must be literal values.
> All *other* fields in your config (API keys, MCP server env vars, agent variables, etc.) support `{{SECRET_NAME}}`
> references as normal.
# First-Run Setup
The first time you start Coyote without a config file, a wizard walks you through picking a secrets provider:
1. Choose a provider from the menu (Local, AWS, GCP, Azure, gopass, 1Password).
2. Coyote prompts you for the provider-specific config (AWS profile/region, GCP project ID, Azure vault name, etc.).
3. For non-Local providers, Coyote performs a **round-trip validation**: it writes a probe secret to the backend, reads
it back, then deletes it. If your credentials don't have the right permissions, or if you're not logged in, Coyote
bails out *before* you fill out the rest of the wizard, with a hint pointing to the correct login command.
4. For the Local provider, Coyote prompts you to create a password file.
Once the provider is set up, the wizard continues with your LLM/API provider selection and writes your `config.yaml`.
If you set up Coyote with one provider and later want to switch, just edit your `config.yaml` to change (or add) the
`secrets_provider` block.
# Motivation
Coyote is intended to be highly configurable and adaptable to many different use cases. This means that users of Coyote
should be able to share configurations for agents, tools, roles, etc. with other users or even entire teams.
@@ -40,29 +130,24 @@ should be able to share configurations for agents, tools, roles, etc. with other
My objective is to encourage this, and to make it so that users can easily version their configurations using version
control. Good VCS hygiene dictates that one *never* commits secrets or sensitive information to a repository.
Since a number of files and configurations in Coyote may contain sensitive information, the vault exists to solve this problem.
Since a number of files and configurations in Coyote may contain sensitive information, the vault exists to solve this
problem. How you share secrets across a team depends on your provider:
Users can either share the vault password with a team, making it so a single configuration can be pulled from VCS and used
by said team. Alternatively, each user can maintain their own vault password and expect other users to replace secret values
with their user-specific secrets.
- **Local:** Either share the vault password with the team (one config + one shared password file) or have each user
maintain their own password and substitute their own secret values.
- **AWS / GCP / Azure / gopass / 1Password:** Each team member uses their own credentials against the shared backend.
The vault becomes a natural single source of truth. Rotating a secret in one place propagates to everyone using
that config.
# How it works
When you first start Coyote, if you don't already have a vault password file, it will prompt you to create one. This file
houses the password that is used to encrypt and decrypt secrets within Coyote. This file exists so that you are not prompted
for a password every time Coyote attempts to decrypt a secret.
When you encrypt a secret, it uses the local provider for `gman` to securely store those secrets in the Coyote vault file.
This file is typically located at your Coyote configuration directory under `vault.yml`. If you open this file, you'll see a
bunch of gibberish. This is because all secrets are encrypted using the password you provided, meaning only you can decrypt them.
Secrets are specified in Coyote configurations using the same variable templating as the [Jinja templating engine](https://jinja.palletsprojects.com/en/stable/):
# Referencing Secrets
Secrets are referenced in Coyote configurations using the same variable templating as the [Jinja templating engine](https://jinja.palletsprojects.com/en/stable/):
```
{{some_variable}}
```
So whenever you want Coyote to use a secret from the vault, you simply specify the secret name in this format in the applicable
file.
So whenever you want Coyote to use a secret from the vault, you simply specify the secret name in this format in the
applicable file. The same syntax works regardless of which provider stores the secret.
**Example:**
Suppose my vault has a secret called `GITHUB_TOKEN` in it, and I want to use that in the MCP configuration. Then, I simply replace
@@ -93,16 +178,17 @@ the expected value in my `mcp.json` with the templated secret:
}
```
At runtime, Coyote will detect the templated secret and replace it with the decrypted value from the vault before executing.
At runtime, Coyote will detect the templated secret and replace it with the decrypted value from the vault before
executing.
# Supported Files
At the time of writing, the following files support Coyote secret injection:
| File Type | Description | Limitations |
|-------------------------|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
| `config.yaml` | The main Coyote configuration file | Cannot use secret injection on the `vault_password_file` field |
| `functions/mcp.json` | The MCP server configuration file | |
| `<agent>/tools.<py/sh>` | Tool files for agents | Specific configuration and only supported for Agents, not all global tools ([see below](#environment-variable-secret-injection-in-agents)) |
| File Type | Description | Limitations |
|-------------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| `config.yaml` | The main Coyote configuration file | Cannot use secret injection on the `vault_password_file` field or anywhere inside the `secrets_provider` block |
| `functions/mcp.json` | The MCP server configuration file | |
| `<agent>/tools.<py/sh>` | Tool files for agents | Specific configuration and only supported for Agents, not all global tools ([see below](#environment-variable-secret-injection-in-agents)) |
Note that all paths are relative to the Coyote configuration directory. The directory varies by system, so you can find yours by
@@ -146,4 +232,3 @@ follows:
```
For more information about variable usage within agents, refer to the [Variables section](Agents#user-defined-variables) of the [Agents documentation](Agents)