Skip to content
Open
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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ These apply repo-wide; module guides own the module-specific detail.
frontend tests live in `frontend/tests/`.
- **Format before pushing** — run `make format` (backend) / `pnpm check` (frontend). Backend
CI enforces `ruff format --check`, so formatting must be clean before a push.
- **Skill text encoding** — treat `SKILL.md` and other textual skill resources as UTF-8;
Python utilities that read them must pass `encoding="utf-8"` rather than relying on the
platform locale.
- **Version sources must stay in lockstep** — a release version must match identically in
`backend/pyproject.toml`, `frontend/package.json`, and `deploy/helm/deer-flow/Chart.yaml`
(`version` + `appVersion`). Pushing a `v*` git tag triggers CI that runs
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,8 @@ Skills are loaded progressively — only when the task needs them, not all at on

A skill directory is a package boundary: once DeerFlow finds its `SKILL.md`, nested `SKILL.md` files under that package (for example evaluation fixtures) remain supporting data and are not registered as runtime skills. Namespace directories without their own `SKILL.md` can still group nested skills.

Skill Markdown and bundled text resources use UTF-8. Validation helpers read them explicitly as UTF-8 so localized skills behave consistently across operating systems.

Users can explicitly activate an enabled skill for a single turn by starting the request with `/skill-name`, for example `/data-analysis analyze uploads/foo.csv`. DeerFlow loads that skill's `SKILL.md` as hidden current-turn context while leaving the base prompt limited to skill metadata. Slash activation respects disabled skills, custom-agent skill whitelists, and existing channel commands such as `/new` and `/help`.

An enabled skill's `allowed-tools` policy applies only after that skill is explicitly slash-activated or captured in the agent's active skill context after a `read_file` load. Merely enabling, advertising, or listing a skill in a custom agent or subagent `skills` allowlist does not reduce that agent's normal toolset; subagents use the same progressive discovery and activation policy as the lead agent. During a slash-activated run, that explicit skill's policy is authoritative: reading another `SKILL.md` may provide instructions but cannot widen the slash skill's tools. Without slash activation, policies from skills actually loaded into active context retain their union semantics. Once active, the policy filters both model-visible tool schemas and tool execution. Framework discovery tools (`tool_search` and `describe_skill`) remain available so an allowed deferred tool or installed skill can still be discovered, but discovery and promotion never grant permission to execute a business tool omitted from `allowed-tools`. `task` is not framework-exempt; a restrictive skill must list it explicitly to delegate to a subagent. Per-step policy decisions are internal runtime context and are removed from observable or persisted context copies. Registry failures and an active set with no remaining valid skill fail closed to framework-safe tools; individual stale paths are ignored only when another valid active skill remains. This is best-effort behavioral scoping, not a hard security boundary: loading skill instructions through another tool is not captured, and active-skill entries can be evicted from bounded context.
Expand Down
4 changes: 2 additions & 2 deletions skills/public/skill-creator/scripts/quick_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def validate_skill(skill_path):
return False, "SKILL.md not found"

# Read and validate frontmatter
content = skill_md.read_text()
content = skill_md.read_text(encoding="utf-8")
if not content.startswith('---'):
return False, "No YAML frontmatter found"

Expand Down Expand Up @@ -99,4 +99,4 @@ def validate_skill(skill_path):

valid, message = validate_skill(sys.argv[1])
print(message)
sys.exit(0 if valid else 1)
sys.exit(0 if valid else 1)
38 changes: 38 additions & 0 deletions tests/skills/test_skill_creator_quick_validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations

import importlib.util
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[2]
VALIDATOR_PATH = REPO_ROOT / "skills" / "public" / "skill-creator" / "scripts" / "quick_validate.py"


def _load_validator():
spec = importlib.util.spec_from_file_location("deerflow_skill_creator_quick_validate", VALIDATOR_PATH)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def test_validate_skill_reads_markdown_as_utf8(tmp_path: Path, monkeypatch) -> None:
validator = _load_validator()
skill_dir = tmp_path / "localized-skill"
skill_dir.mkdir()
skill_md = skill_dir / "SKILL.md"
skill_md.write_text(
"---\nname: localized-skill\ndescription: 处理中文内容\n---\n\n# 中文技能\n",
encoding="utf-8",
)

original_read_text = Path.read_text

def require_explicit_encoding(self: Path, encoding: str | None = None, errors: str | None = None) -> str:
if self == skill_md and encoding is None:
raise UnicodeDecodeError("gbk", b"\x80", 0, 1, "illegal multibyte sequence")
return original_read_text(self, encoding=encoding, errors=errors)

monkeypatch.setattr(Path, "read_text", require_explicit_encoding)

assert validator.validate_skill(skill_dir) == (True, "Skill is valid!")
Loading