mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
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
This commit is contained in:
parent
376d82fec7
commit
f1250f0d16
@ -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 = (
|
||||
|
||||
@ -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 ----------------------------------------------------------
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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/<name>/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.
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user