A project-scoped agent definition from an untrusted repository could declare inline stdio mcpServers and the child session started that command, while inline hooks on the same definition were already gated on folder trust. The mcpServers list now uses the same gate. By-name agent lookup joined the requested name onto each searched directory, so a name carrying a separator or ".." resolved a file outside that directory under its trust scope; both lookup entry points now reject any name that is not a single path component. Ported from upstream (grok-build subagent/handle_request.rs mcpServers gate), extended with the name guard. Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
kigi-agent
Agent builder, definition parsing, and system prompt assembly.
This crate extracts a first-class Agent type from kigi-shell.
An Agent bundles tools, system prompt, system-reminder policy,
compaction policy, and model configuration into a single, portable
object that any host can consume — whether that host is
kigi-shell, another in-process host, or a headless batch runner.
Quick Start
From a definition file
Agent definitions are Markdown files with YAML frontmatter, stored
in .kigi/agents/ (project-level) or ~/.kigi/agents/ (user-level).
use kigi_agent::{AgentDefinition, AgentBuilder};
use kigi_tools::notification::ToolNotificationHandle;
// 1. Parse the definition file
let def = AgentDefinition::from_file(".kigi/agents/code-reviewer.md")?;
// 2. Build the agent
let agent = AgentBuilder::new(cwd, None, ToolNotificationHandle::noop())
.from_definition(def)
.build()
.await?;
// 3. Use it
println!("Agent: {}", agent.name());
println!("Prompt: {}", agent.system_prompt());
let tool_defs = agent.tool_definitions().await;
Programmatic (no file)
let agent = AgentBuilder::new(cwd, None, ToolNotificationHandle::noop())
.with_name("my-agent")
.with_description("A custom agent")
.with_tools(vec!["read_file".into(), "grep".into()])
.build()
.await?;
Discover all definitions
use kigi_agent::discovery;
// Find all .md files in .kigi/agents/ directories
let definitions = discovery::discover(&cwd);
// Find a specific agent by name (checks built-ins, then user dirs)
let reviewer = discovery::by_name("code-reviewer");
// Find with project-level priority
let agent = discovery::by_name_in_cwd("my-agent", &cwd);
Agent Definition File Format
Agent definitions are Markdown files with YAML frontmatter:
---
name: my-agent
description: What this agent does
# ... additional config fields
---
System prompt body goes here...
The frontmatter (between --- delimiters) is YAML configuration.
The body (after the closing ---) is the system prompt content.
Minimal example (extends base template)
---
name: code-reviewer
description: Reviews code for quality and security
tools:
- read_file
- grep
- list_dir
permissionMode: plan
---
You are a senior code reviewer. Analyze code and provide
actionable feedback organized by severity.
With promptMode: extend (the default), the body is appended to the
base template which includes tool calling conventions, formatting
rules, and user info. The author only writes persona-specific content.
Full prompt override
---
name: custom-agent
description: Agent with full control over the system prompt
promptMode: full
tools:
- read_file
- search_replace
- run_terminal_cmd
---
You are a custom agent.
Use ${{ tools.read_file }} to read files.
Use ${{ tools.search_replace }} to edit files.
${%- if tools.run_terminal_cmd %}
Use ${{ tools.run_terminal_cmd }} for shell commands.
${%- endif %}
<user_info>
OS: ${{ os_name }}
Shell: ${{ shell_path }}
Working Directory: ${{ working_directory }}
Date: ${{ current_date }}
</user_info>
With promptMode: full, the body IS the complete system prompt,
rendered through MiniJinja with custom ${{ }}/${% %} delimiters
(to avoid collisions with literal {{ }} in prose).
With completion requirement (orchestrated mode)
---
name: orchestrator-worker
description: Worker agent that must signal completion before ending a turn
completionRequirement:
tool: complete_task
reminder: >
You stopped without calling `complete_task`.
Please continue and call it when done.
recovery:
maxRetries: 5
baseDelayMs: 5000
maxDelayMs: 60000
toolConfig:
wait_for_instruction:
retry:
maxRetries: 1440
baseDelayMs: 5000
maxDelayMs: 30000
---
You are a worker agent in an orchestrated multi-agent workflow.
You MUST call `complete_task` before ending your response.
Frontmatter Schema Reference
All frontmatter keys use camelCase.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
Yes | — | Unique agent ID (lowercase, hyphens) |
description |
string |
Yes | — | When/why to use this agent |
promptMode |
string |
No | "extend" |
"extend" or "full" |
tools |
string[] |
No | inherit all | Tool allowlist. Omit = all tools. [] = none |
disallowedTools |
string[] |
No | [] |
Denylist (takes priority over tools) |
permissionMode |
string |
No | "default" |
"default", "acceptEdits", "dontAsk", "plan" |
skills |
string[] |
No | [] |
Skill names to pre-load |
agentsMd |
bool |
No | true |
Discover and inject AGENTS.md files |
outputFormat |
string |
No | "default" |
"default" or "concise" |
bash |
object |
No | defaults | Bash tool config overrides |
bash.timeoutSecs |
float |
No | 120.0 |
Bash command timeout |
bash.outputByteLimit |
int |
No | 200000 |
Max output bytes |
bash.cmdPrefix |
string |
No | null |
Command prefix |
toolNameOverrides |
map<string,string> |
No | {} |
Canonical → model-facing name map |
paramNameOverrides |
map<string,map> |
No | {} |
Per-tool param name map |
completionRequirement |
object |
No | null |
Tool that must be called before turn ends |
completionRequirement.tool |
string |
Yes* | — | Canonical tool name |
completionRequirement.reminder |
string |
Yes* | — | Reminder text when not called |
completionRequirement.recovery |
object |
No | null |
Recovery policy for the harness |
toolConfig |
map<string,object> |
No | {} |
Per-tool execution config |
toolConfig.*.retry |
object |
No | null |
Retry config for a tool |
*Required only when completionRequirement is set.
Prompt Assembly
promptMode: extend promptMode: full
────────────────── ─────────────────
1. Base template (MiniJinja) 1. Markdown body (MiniJinja, ${{ }}/${% %})
(tool conventions, formatting, 2. AGENTS.md section (if agentsMd: true)
user_info, background tasks) 3. Skills section
2. Markdown body (appended raw)
3. AGENTS.md section (if agentsMd: true)
4. Skills section
Template Variables (full mode)
| Variable | Description |
|---|---|
${{ tools.read_file }} |
Resolved name for read_file (or empty if disabled) |
${{ tools.search_replace }} |
Resolved name for search_replace |
${{ tools.run_terminal_cmd }} |
Resolved name for run_terminal_cmd |
${{ tools.grep }} |
Resolved name for grep |
${{ tools.list_dir }} |
Resolved name for list_dir |
${{ tools.todo_write }} |
Resolved name for todo_write |
${{ tools.skill }} |
Resolved name for skill |
${{ tools.get_task_output }} |
Resolved name for get_task_output |
${{ tools.kill_task }} |
Resolved name for kill_task |
${{ tools.web_search }} |
Resolved name for web_search |
${{ os_name }} |
Operating system (e.g. "macos", "linux") |
${{ shell_path }} |
Shell path (e.g. "/bin/zsh") |
${{ working_directory }} |
Workspace path |
${{ current_date }} |
Current date in the user's local timezone (YYYY-MM-DD) |
Conditionals: ${%- if tools.todo_write %}...${%- endif %} — block
is omitted when the tool is disabled.
Discovery Rules
Agent definitions are discovered from multiple locations with priority:
- Project-level (highest priority):
.kigi/agents/*.md— walk fromcwdup to the git repository root. Files found closer tocwdtake priority. - User-level:
~/.kigi/agents/*.md - Compat paths (lowest priority): additional vendor agent directories under the user home (when enabled)
- Built-in:
default_kigi(),browser_use()
Name-based dedup ensures the highest-priority definition wins. For
example, a project .kigi/agents/code-reviewer.md shadows a
user-level definition with the same name.
Crate Relationships
┌──────────────────┐
│ kigi-agent │ ← This crate
│ (Agent, Builder, │
│ Definition) │
└────────┬─────────┘
│ depends on
▼
┌──────────────────┐
│ kigi-tools │
│ (ToolBridge, │
│ ToolRegistry, │
│ ToolState) │
└────────▲─────────┘
│ depends on
┌────────┴─────────┐
│ kigi-shell │ uses AgentBuilder to create
│ (session host) │ Agent during session setup
└──────────────────┘
kigi-tools: ProvidesToolBridge,ToolRegistry,ToolState,SystemReminderLayer, and tool implementations.kigi-agentdepends on it for tool setup.kigi-shell: The application shell. UsesAgentBuilderto construct anAgentduring session creation. The shell re-exports some modules fromkigi-agent(AGENTS.md discovery, skills discovery, base prompt rendering).
Built-in Agents
| Name | Prompt Mode | Description |
|---|---|---|
kigi |
extend | Default agent for software engineering tasks |
browser-use |
full | Web browsing and interaction agent |
Error Handling
AgentBuilder::build() returns Result<Agent, AgentBuildError>:
| Error | When |
|---|---|
ParseError |
Bad YAML, missing ---, wrong types |
MissingField |
Required field (name/description) absent |
UnknownToolOverride |
toolNameOverrides references nonexistent tool |
IoError |
File read error during AGENTS.md/skills discovery |
MiniJinjaError |
Template rendering failure |
Unknown frontmatter fields are silently ignored for forward compatibility — definitions written for newer versions work on older ones.
Development
# Check
cargo check -p kigi-agent
# Test
cargo test -p kigi-agent
# Clippy
cargo clippy -p kigi-agent --fix --allow-dirty
# Format
cargo fmt --all