From f1250f0d16cb1884a9b207b073c8135d82a52404 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Tue, 26 May 2026 00:50:45 +0800 Subject: [PATCH] feat(dream): add skill ownership guard with dream_managed frontmatter - Dream-created skills get in SKILL.md frontmatter - tags each skill as [dream]/[user]/[builtin] - New config (default False): when False Dream can only modify [dream] skills; when True [user] skills are also editable - [builtin] skills are never editable - dream.md prompt uses Jinja conditional to render edit policy - WebUI types and test fixtures updated with new config field --- nanobot/agent/loop.py | 3 ++- nanobot/agent/memory.py | 41 ++++++++++++++++++++++++----- nanobot/cli/commands.py | 1 + nanobot/config/schema.py | 4 +++ nanobot/templates/agent/dream.md | 9 ++++++- nanobot/webui/settings_api.py | 1 + webui/src/lib/types.ts | 1 + webui/src/tests/app-layout.test.tsx | 3 +++ 8 files changed, 54 insertions(+), 9 deletions(-) diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index fcd784073..df89e6aa5 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -1168,6 +1168,7 @@ class AgentLoop: user_path=str(workspace / "USER.md"), memory_path=str(workspace / "memory" / "MEMORY.md"), stale_threshold_days=_STALE_THRESHOLD_DAYS, + dream_edit_user_skills=self.dream.edit_user_skills, ) session.metadata["_dream_system_prompt"] = cached_prompt session.metadata["_dream_system_prompt_mtime"] = current_mtime @@ -1220,7 +1221,7 @@ class AgentLoop: f"## Current USER.md ({len(current_user)} chars)\n{current_user}" ) - existing_skills = self.dream._list_existing_skills() + existing_skills = self.dream._list_existing_skills(tag_origin=True) skills_section = "" if existing_skills: skills_section = ( diff --git a/nanobot/agent/memory.py b/nanobot/agent/memory.py index b88fff786..eb22c47ea 100644 --- a/nanobot/agent/memory.py +++ b/nanobot/agent/memory.py @@ -986,6 +986,7 @@ class Dream: max_iterations: int = 10, max_tool_result_chars: int = 16_000, annotate_line_ages: bool = True, + edit_user_skills: bool = False, ): self.store = store self.provider = provider @@ -997,6 +998,9 @@ class Dream: # Default True keeps the #3212 behavior; set False to feed all memory # files raw (e.g. if a specific LLM reacts poorly to the `← Nd` suffix). self.annotate_line_ages = annotate_line_ages + # When True, Dream may edit/delete user-created workspace skills. + # When False, only skills with dream_managed: true in frontmatter are editable. + self.edit_user_skills = edit_user_skills self._runner = AgentRunner(provider) self._tools = self._build_tools() @@ -1038,15 +1042,25 @@ class Dream: # -- skill listing -------------------------------------------------------- - def _list_existing_skills(self) -> list[str]: - """List existing skills as 'name — description' for dedup context.""" + def _list_existing_skills(self, tag_origin: bool = False) -> list[str]: + """List existing skills as 'name — description [origin]' for dedup context. + + When *tag_origin* is True each entry gets an origin tag: + ``[dream]`` for skills with ``dream_managed: true`` in frontmatter, + ``[user]`` for other workspace skills, ``[builtin]`` for bundled skills. + """ import re as _re from nanobot.agent.skills import BUILTIN_SKILLS_DIR desc_re = _re.compile(r"^description:\s*(.+)$", _re.MULTILINE | _re.IGNORECASE) - entries: dict[str, str] = {} - for base in (self.store.workspace / "skills", BUILTIN_SKILLS_DIR): + managed_re = _re.compile(r"^dream_managed:\s*true$", _re.MULTILINE | _re.IGNORECASE) + + entries: dict[str, tuple[str, str]] = {} # name -> (desc, tag) + builtin_dir = BUILTIN_SKILLS_DIR + ws_skills_dir = self.store.workspace / "skills" + + for base in (ws_skills_dir, builtin_dir): if not base.exists(): continue for d in base.iterdir(): @@ -1056,13 +1070,26 @@ class Dream: if not skill_md.exists(): continue # Prefer workspace skills over builtin (same name) - if d.name in entries and base == BUILTIN_SKILLS_DIR: + if d.name in entries and base == builtin_dir: continue content = skill_md.read_text(encoding="utf-8")[:500] m = desc_re.search(content) desc = m.group(1).strip() if m else "(no description)" - entries[d.name] = desc - return [f"{name} — {desc}" for name, desc in sorted(entries.items())] + + if tag_origin: + if base == builtin_dir: + tag = "[builtin]" + elif managed_re.search(content): + tag = "[dream]" + else: + tag = "[user]" + entries[d.name] = (desc, tag) + else: + entries[d.name] = (desc, "") + + if tag_origin: + return [f"{name} — {desc} {tag}" for name, (desc, tag) in sorted(entries.items())] + return [f"{name} — {desc}" for name, (desc, _) in sorted(entries.items())] # -- main entry ---------------------------------------------------------- diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 2a3d4f484..1cd2b9438 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -1030,6 +1030,7 @@ def _run_gateway( agent.dream.max_batch_size = dream_cfg.max_batch_size agent.dream.max_iterations = dream_cfg.max_iterations agent.dream.annotate_line_ages = dream_cfg.annotate_line_ages + agent.dream.edit_user_skills = dream_cfg.dream_edit_user_skills from nanobot.cron.types import CronJob, CronPayload cron.register_system_job(CronJob( id="dream", diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index e5aed3494..865ebab76 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -59,6 +59,10 @@ class DreamConfig(Base): # on — set to False to feed all memory files raw if a specific LLM reacts # poorly to the `← Nd` suffix or you want deterministic, git-independent prompts. annotate_line_ages: bool = True + # When False (default), Dream may only modify skills it created (marked + # dream_managed in frontmatter). When True, Dream may also edit user-created + # workspace skills. Builtin skills are never editable. + dream_edit_user_skills: bool = False def build_schedule(self, timezone: str) -> CronSchedule: """Build the runtime schedule, preferring the legacy cron override if present.""" diff --git a/nanobot/templates/agent/dream.md b/nanobot/templates/agent/dream.md index eeb004bc6..d98350e36 100644 --- a/nanobot/templates/agent/dream.md +++ b/nanobot/templates/agent/dream.md @@ -45,10 +45,17 @@ Flag [SKILL] only when ALL are true: repeatable workflow appeared 2+ times, invo For [SKILL] entries: - Use write_file to create skills//SKILL.md; read_file `{{ skill_creator_path }}` for format reference -- YAML frontmatter (name, description), under 2000 words: when to use, steps, output format, example +- YAML frontmatter must include name, description, **and `dream_managed: true`** (marks this skill as Dream-created) +- Under 2000 words: when to use, steps, output format, example - Do NOT overwrite existing skills — if overlapping, merge delta into the existing skill - Skills are instruction sets, not code. Keep concrete values in MEMORY.md; skills use placeholders +## Skill edit policy +Each skill in the Existing Skills list is tagged with an origin: +- **[dream]** — Dream-created (has `dream_managed: true` in frontmatter). You MAY edit these. +- **[user]** — User-created workspace skill. {% if dream_edit_user_skills %}You MAY edit these.{% else %}You MUST NOT modify, rename, or delete these — you can only read them for context.{% endif %} +- **[builtin]** — Bundled with nanobot. You MUST NEVER modify these. + ## Editing - Default tool: apply_patch. Use edit_file only for small exact replacements. - File contents provided below — no read_file needed for initial edits. diff --git a/nanobot/webui/settings_api.py b/nanobot/webui/settings_api.py index 7e093a5e2..6e5bf0c78 100644 --- a/nanobot/webui/settings_api.py +++ b/nanobot/webui/settings_api.py @@ -307,6 +307,7 @@ def settings_payload(*, requires_restart: bool = False) -> dict[str, Any]: "max_batch_size": defaults.dream.max_batch_size, "max_iterations": defaults.dream.max_iterations, "annotate_line_ages": defaults.dream.annotate_line_ages, + "dream_edit_user_skills": defaults.dream.dream_edit_user_skills, }, "unified_session": defaults.unified_session, }, diff --git a/webui/src/lib/types.ts b/webui/src/lib/types.ts index d8b181e3d..2740801c4 100644 --- a/webui/src/lib/types.ts +++ b/webui/src/lib/types.ts @@ -265,6 +265,7 @@ export interface SettingsPayload { max_batch_size: number; max_iterations: number; annotate_line_ages: boolean; + dream_edit_user_skills: boolean; }; unified_session: boolean; }; diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx index d6f228838..22e7981ef 100644 --- a/webui/src/tests/app-layout.test.tsx +++ b/webui/src/tests/app-layout.test.tsx @@ -92,6 +92,7 @@ function baseSettingsPayload() { max_batch_size: 20, max_iterations: 15, annotate_line_ages: true, + dream_edit_user_skills: false, }, unified_session: false, }, @@ -780,6 +781,7 @@ describe("App layout", () => { max_batch_size: 20, max_iterations: 15, annotate_line_ages: true, + dream_edit_user_skills: false, }, unified_session: false, }, @@ -1066,6 +1068,7 @@ describe("App layout", () => { max_batch_size: 20, max_iterations: 15, annotate_line_ages: true, + dream_edit_user_skills: false, }, unified_session: false, },