Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Body sanitization applied automatically to all non-`Trusted` skills in `format_skills_prompt()` (#689)

### Changed
- `requires-secrets` SKILL.md frontmatter field renamed to `x-requires-secrets` to follow JSON Schema vendor extension convention and avoid future spec collisions — **breaking change**: update skill frontmatter to use `x-requires-secrets`; the old `requires-secrets` form is still parsed with a deprecation warning (#688)
- `allowed-tools` SKILL.md field now uses space-separated values per agentskills.io spec (was comma-separated) — **breaking change** for skills using comma-delimited allowed-tools (#686)

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ Skills **evolve**: failure detection triggers self-reflection, and the agent gen

**External skill management**: install, remove, verify, and control trust for skills via `zeph skill` CLI subcommands or in-session `/skill install` and `/skill remove` commands with automatic hot-reload. Managed skills are stored in `~/.config/zeph/skills/`.

Skills can declare **required secrets** via the `requires-secrets` frontmatter field. Zeph resolves each named secret from the vault and injects it as an environment variable scoped to tool execution for that skill — no hardcoded credentials, no secret leakage across skills. Store custom secrets under the `ZEPH_SECRET_<NAME>` key; the `zeph init` wizard includes a dedicated step for this.
Skills can declare **required secrets** via the `x-requires-secrets` frontmatter field. Zeph resolves each named secret from the vault and injects it as an environment variable scoped to tool execution for that skill — no hardcoded credentials, no secret leakage across skills. Store custom secrets under the `ZEPH_SECRET_<NAME>` key; the `zeph init` wizard includes a dedicated step for this.

[Self-learning →](https://bug-ops.github.io/zeph/guide/self-learning.html) · [Skill trust →](https://bug-ops.github.io/zeph/guide/skill-trust.html)

Expand Down
4 changes: 2 additions & 2 deletions crates/zeph-core/src/agent/tool_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1547,7 +1547,7 @@ mod tests {
std::fs::create_dir(&skill_dir).unwrap();
std::fs::write(
skill_dir.join("SKILL.md"),
"---\nname: gh-skill\ndescription: GitHub.\nrequires-secrets: github_token\n---\nbody",
"---\nname: gh-skill\ndescription: GitHub.\nx-requires-secrets: github_token\n---\nbody",
)
.unwrap();
let registry = SkillRegistry::load(&[temp_dir.path().to_path_buf()]);
Expand Down Expand Up @@ -1587,7 +1587,7 @@ mod tests {
std::fs::create_dir(&skill_dir).unwrap();
std::fs::write(
skill_dir.join("SKILL.md"),
"---\nname: tok-skill\ndescription: Token.\nrequires-secrets: api_token\n---\nbody",
"---\nname: tok-skill\ndescription: Token.\nx-requires-secrets: api_token\n---\nbody",
)
.unwrap();
let registry = SkillRegistry::load(&[temp_dir.path().to_path_buf()]);
Expand Down
48 changes: 46 additions & 2 deletions crates/zeph-skills/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ struct RawFrontmatter {
metadata: Vec<(String, String)>,
allowed_tools: Vec<String>,
requires_secrets: Vec<String>,
/// Whether `requires-secrets` (deprecated) was used instead of `x-requires-secrets`.
deprecated_requires_secrets: bool,
}

fn parse_frontmatter(yaml_str: &str) -> RawFrontmatter {
Expand All @@ -83,6 +85,7 @@ fn parse_frontmatter(yaml_str: &str) -> RawFrontmatter {
let mut metadata = Vec::new();
let mut allowed_tools = Vec::new();
let mut requires_secrets = Vec::new();
let mut deprecated_requires_secrets = false;
let mut in_metadata = false;

for line in yaml_str.lines() {
Expand Down Expand Up @@ -123,13 +126,25 @@ fn parse_frontmatter(yaml_str: &str) -> RawFrontmatter {
"allowed-tools" => {
allowed_tools = value.split_whitespace().map(ToString::to_string).collect();
}
"requires-secrets" => {
"x-requires-secrets" => {
requires_secrets = value
.split(',')
.map(|s| s.trim().to_lowercase().replace('-', "_"))
.filter(|s| !s.is_empty())
.collect();
}
"requires-secrets" => {
deprecated_requires_secrets = true;
// Only apply if x-requires-secrets was not already parsed.
// The canonical x-requires-secrets always wins over the deprecated form.
if requires_secrets.is_empty() {
requires_secrets = value
.split(',')
.map(|s| s.trim().to_lowercase().replace('-', "_"))
.filter(|s| !s.is_empty())
.collect();
}
}
"metadata" if value.is_empty() => {
in_metadata = true;
}
Expand All @@ -150,6 +165,7 @@ fn parse_frontmatter(yaml_str: &str) -> RawFrontmatter {
metadata,
allowed_tools,
requires_secrets,
deprecated_requires_secrets,
}
}

Expand Down Expand Up @@ -250,6 +266,13 @@ pub fn load_skill_meta(path: &Path) -> Result<SkillMeta, SkillError> {
validate_skill_name(&name, dir_name)
.map_err(|e| SkillError::Other(format!("in {}: {e}", path.display())))?;

if raw.deprecated_requires_secrets {
tracing::warn!(
"'requires-secrets' is deprecated, use 'x-requires-secrets' in {}",
path.display()
);
}

Ok(SkillMeta {
name,
description,
Expand Down Expand Up @@ -626,17 +649,38 @@ mod tests {
}

#[test]
fn requires_secrets_parsed_from_frontmatter() {
fn x_requires_secrets_parsed_from_frontmatter() {
let dir = tempfile::tempdir().unwrap();
let path = write_skill(
dir.path(),
"github-api",
"---\nname: github-api\ndescription: GitHub integration.\nx-requires-secrets: github-token, github-org\n---\nbody",
);
let meta = load_skill_meta(&path).unwrap();
assert_eq!(meta.requires_secrets, vec!["github_token", "github_org"]);
}

#[test]
fn requires_secrets_deprecated_backward_compat() {
let dir = tempfile::tempdir().unwrap();
let path = write_skill(
dir.path(),
"github-api",
"---\nname: github-api\ndescription: GitHub integration.\nrequires-secrets: github-token, github-org\n---\nbody",
);
// Old form still works (backward compat), but emits a deprecation warning.
let meta = load_skill_meta(&path).unwrap();
assert_eq!(meta.requires_secrets, vec!["github_token", "github_org"]);
}

#[test]
fn x_requires_secrets_takes_precedence_over_deprecated() {
// When both are present, x-requires-secrets wins regardless of order.
let raw = parse_frontmatter("x-requires-secrets: key_a\nrequires-secrets: key_b\n");
assert_eq!(raw.requires_secrets, vec!["key_a"]);
assert!(raw.deprecated_requires_secrets);
}

#[test]
fn requires_secrets_empty_by_default() {
let dir = tempfile::tempdir().unwrap();
Expand Down
2 changes: 1 addition & 1 deletion crates/zeph-skills/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ mod tests {
std::fs::create_dir_all(&skill_dir).unwrap();
std::fs::write(
skill_dir.join("SKILL.md"),
"---\nname: api-skill\ndescription: Needs secrets.\nrequires-secrets: github_token, slack_webhook\n---\n# Body\nHello",
"---\nname: api-skill\ndescription: Needs secrets.\nx-requires-secrets: github_token, slack_webhook\n---\n# Body\nHello",
)
.unwrap();

Expand Down
2 changes: 1 addition & 1 deletion crates/zeph-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Defines the `ToolExecutor` trait for sandboxed tool invocation and ships concret
| Module | Description |
|--------|-------------|
| `executor` | `ToolExecutor` trait, `ToolOutput`, `ToolCall` |
| `shell` | Shell command executor with tokenizer-based command detection, escape normalization, and transparent wrapper skipping; receives skill-scoped env vars injected by the agent for active skills that declare `requires-secrets` |
| `shell` | Shell command executor with tokenizer-based command detection, escape normalization, and transparent wrapper skipping; receives skill-scoped env vars injected by the agent for active skills that declare `x-requires-secrets` |
| `file` | File operation executor |
| `scrape` | Web scraping executor with SSRF protection (post-DNS private IP validation, pinned address client) |
| `composite` | `CompositeExecutor` — chains executors with middleware |
Expand Down
2 changes: 1 addition & 1 deletion docs/src/concepts/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Use `/skills` in chat to see active skills and their usage statistics.
- **Progressive loading**: only metadata (~100 tokens per skill) is loaded at startup. Full body is loaded on first activation and cached
- **Hot-reload**: edit a `SKILL.md` file, changes apply without restart
- **Two matching backends**: in-memory (default) or Qdrant (faster startup with many skills, delta sync via BLAKE3 hash)
- **Secret gating**: skills that declare `requires-secrets` in their frontmatter are excluded from the prompt if the required secrets are not present in the vault. This prevents the agent from attempting to use a skill that would fail due to missing credentials
- **Secret gating**: skills that declare `x-requires-secrets` in their frontmatter are excluded from the prompt if the required secrets are not present in the vault. This prevents the agent from attempting to use a skill that would fail due to missing credentials

## External Skill Management

Expand Down
2 changes: 1 addition & 1 deletion docs/src/getting-started/wizard.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Skip this step if you do not plan to run Zeph in headless mode.

If the `age` vault backend was selected, the wizard offers to add custom secrets for skill authentication.

When prompted, enter a secret name and value. The wizard stores each secret with the `ZEPH_SECRET_` prefix in the vault. If any installed skills declare `requires-secrets`, the wizard lists them so you know which keys to provide.
When prompted, enter a secret name and value. The wizard stores each secret with the `ZEPH_SECRET_` prefix in the vault. If any installed skills declare `x-requires-secrets`, the wizard lists them so you know which keys to provide.

Skip this step if your skills do not require external API credentials.

Expand Down
10 changes: 5 additions & 5 deletions docs/src/guides/custom-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,24 @@ into the LLM context when the skill is matched.
| `description` | Yes | Used for embedding-based matching against user queries |
| `compatibility` | No | Runtime requirements (e.g., "requires curl") |
| `allowed-tools` | No | Space-separated tool names this skill can use |
| `requires-secrets` | No | Comma-separated secret names the skill needs (see below) |
| `x-requires-secrets` | No | Comma-separated secret names the skill needs (see below) |

### Secret-Gated Skills

If a skill requires API credentials or tokens, declare them with `requires-secrets`:
If a skill requires API credentials or tokens, declare them with `x-requires-secrets`:

```markdown
---
name: github-api
description: GitHub API integration — search repos, create issues, review PRs.
requires-secrets: github-token, github-org
x-requires-secrets: github-token, github-org
---
```

Secret names use lowercase with hyphens. They map to vault keys with the `ZEPH_SECRET_` prefix:

| `requires-secrets` name | Vault key | Env var injected |
|------------------------|-----------|-----------------|
| `x-requires-secrets` name | Vault key | Env var injected |
|--------------------------|-----------|-----------------|
| `github-token` | `ZEPH_SECRET_GITHUB_TOKEN` | `GITHUB_TOKEN` |
| `github-org` | `ZEPH_SECRET_GITHUB_ORG` | `GITHUB_ORG` |

Expand Down
4 changes: 2 additions & 2 deletions docs/src/reference/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ zeph vault set ZEPH_SECRET_GITHUB_TOKEN ghp_yourtokenhere
zeph vault set ZEPH_SECRET_STRIPE_KEY sk_live_...
```

Skills declare which secrets they require via `requires-secrets` in their frontmatter. Skills with unsatisfied secrets are excluded from the prompt automatically — they will not be matched or executed until the secret is available.
Skills declare which secrets they require via `x-requires-secrets` in their frontmatter. Skills with unsatisfied secrets are excluded from the prompt automatically — they will not be matched or executed until the secret is available.

When a skill with `requires-secrets` is active, its secrets are injected as environment variables into shell commands it runs. The prefix is stripped and the name is uppercased:
When a skill with `x-requires-secrets` is active, its secrets are injected as environment variables into shell commands it runs. The prefix is stripped and the name is uppercased:

| Vault key | Env var injected |
|-----------|-----------------|
Expand Down
8 changes: 4 additions & 4 deletions skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Instructions and examples go here.

- `name` — unique identifier for the skill
- `description` — used for matching against user queries. Write it so that the embedding model can connect user intent to this skill. Be specific: "Extract structured data from web pages using CSS selectors" works better than "Web stuff"
- `requires-secrets` — optional comma-separated list of secret names this skill needs (e.g. `github-token, npm-token`). Zeph resolves each name from the vault and injects it as an environment variable before running any shell tool for the active skill. Secret name `github-token` maps to env var `GITHUB_TOKEN` (uppercased, hyphens to underscores).
- `x-requires-secrets` — optional comma-separated list of secret names this skill needs (e.g. `github-token, npm-token`). Zeph resolves each name from the vault and injects it as an environment variable before running any shell tool for the active skill. Secret name `github-token` maps to env var `GITHUB_TOKEN` (uppercased, hyphens to underscores).

**Body:** markdown with instructions, code examples, or reference material. This is injected verbatim into the LLM context when the skill is selected.

Expand All @@ -79,7 +79,7 @@ Instructions and examples go here.
---
name: github-release
description: Create GitHub releases and upload assets via the API.
requires-secrets: github-token
x-requires-secrets: github-token
---
# GitHub Release

Expand Down Expand Up @@ -118,7 +118,7 @@ ssh user@server 'sudo systemctl restart myapp'

## Secrets for Skills

Skills can declare secrets they need via `requires-secrets`. Zeph resolves each name from the active vault and injects it as an environment variable scoped to tool execution for that skill. No other skill or tool run sees the injected values.
Skills can declare secrets they need via `x-requires-secrets`. Zeph resolves each name from the active vault and injects it as an environment variable scoped to tool execution for that skill. No other skill or tool run sees the injected values.

**Storing custom secrets:**

Expand All @@ -130,7 +130,7 @@ zeph vault set ZEPH_SECRET_GITHUB_TOKEN ghp_...
export ZEPH_SECRET_GITHUB_TOKEN=ghp_...
```

The `ZEPH_SECRET_` prefix identifies a value as a skill-scoped custom secret. During startup, Zeph scans the vault for all `ZEPH_SECRET_*` keys and builds an in-memory map. The canonical form strips the prefix and lowercases with hyphens (`ZEPH_SECRET_GITHUB_TOKEN` → `github-token`), matching the `requires-secrets` declaration in the skill frontmatter.
The `ZEPH_SECRET_` prefix identifies a value as a skill-scoped custom secret. During startup, Zeph scans the vault for all `ZEPH_SECRET_*` keys and builds an in-memory map. The canonical form strips the prefix and lowercases with hyphens (`ZEPH_SECRET_GITHUB_TOKEN` → `github-token`), matching the `x-requires-secrets` declaration in the skill frontmatter.

The `zeph init` wizard includes a dedicated step for adding custom secrets during first-time setup.

Expand Down
Loading