Skill lifecycle phase 2: skill_manage with origin-scoped rails - #622
Conversation
Phase 2 of docs/design-docs/skill-lifecycle.md — the mutation surface.
- skill_manage: create/patch/edit/delete/write_file/remove_file, one
action-dispatched tool. WriteOrigin is set by the process constructing
the tool server, never the model. Agent origin narrows the blast
radius: workspace skills only, no installed or pinned targets,
read-before-write via a session tracker shared with read_skill, and
delete archives to .archive/ instead of removing. Pin blocks delete
for users and every mutation for agents. Consolidation deletes require
absorbed_into to exist on disk.
- Validation on all origins: name charset, description budget, size
caps, support-file path rails (canonicalization, symlink refusal).
Every mutation reloads the live SkillSet directly.
- skills_list: skills joined with provenance, state, and counters.
- Category discovery: skills/{category}/{name}/SKILL.md now loads.
- Lifecycle controls: POST /agents/skills/{pin,adopt,archive,restore}
and spacebot skill pin|unpin|adopt|archive|restore; skill info shows
provenance and usage. Archives keep prior copies; restore takes the
most recent.
- Branch toolset gains skill_manage (User origin) and skills_list.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
🚧 Files skipped from review as they are similar to previous changes (9)
WalkthroughThis change adds persistent skill management across storage, usage tracking, tools, CLI commands, and API routes. It supports creation, modification, deletion, pinning, adoption, archiving, restoration, metadata listing, and read tracking. ChangesPersistent skill lifecycle
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/tools.rs (1)
893-917: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winConsider
WriteOrigin::Agentfor the branch tool server.This branch server is model-driven, but
SkillManageToolis registered withWriteOrigin::User. For that origin, edits can target instance-level skills anddeleteperforms permanentremove_dir_alldeletion. If branches should keep the autonomous write guards, useWriteOrigin::Agent; otherwise keep this as an explicit intentional policy decision.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/tools.rs` around lines 893 - 917, Review the branch tool server construction and decide the intended write policy for SkillManageTool. If branch operations are autonomous, change the SkillManageTool registration in the server built by the surrounding function to use WriteOrigin::Agent, preserving the existing read tracker; otherwise document or explicitly retain the User origin as an intentional policy choice.
🧹 Nitpick comments (3)
src/prompts/text.rs (1)
99-101: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove the arm into the "Tool Descriptions" block.
The arm sits between the fragment arms. All other
tools/*keys are grouped from Line 153. Move it there to keep the grouping intact.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/prompts/text.rs` around lines 99 - 101, Move the ("en", "tools/skill_manage") match arm from its current position between the fragment arms into the existing "Tool Descriptions" block with the other tools/* entries, preserving its include_str! mapping unchanged.src/api/skills.rs (1)
537-542: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueRestore maps every failure to
404.
restore_skill_dirfails for two distinct reasons: no archived copy exists, and an active skill directory already occupies the name. The second case is a conflict, not a missing resource. A client cannot tell the two apart. Return409 CONFLICTfor the occupied-name case, and document it in theutoipa::pathresponses.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/api/skills.rs` around lines 537 - 542, Update the restore flow around restore_skill_dir so an existing active skill directory is mapped to StatusCode::CONFLICT while an absent archived copy remains StatusCode::NOT_FOUND. Distinguish these failure cases using the returned error, preserve the warning context, and document both 404 and 409 responses in the associated utoipa::path definition.src/skills.rs (1)
449-463: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueThe suffix scan stops at the first gap.
The loop breaks when
{name}-{suffix}is missing. If an intermediate archive copy is removed, later copies are not seen. Example:.archive/deployand.archive/deploy-2exist,deploy-1does not. The scan then selectsdeploy, which is the oldest copy, not the newest.Read the archive directory once and select the entry with the highest suffix instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/skills.rs` around lines 449 - 463, Replace the sequential suffix loop in the archive source-selection logic with a single read of archive_root, then inspect matching entries for name and name-N forms and select the highest numeric suffix. Preserve the plain name as the base candidate and ensure gaps do not prevent later archive copies from being selected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/api/skills.rs`:
- Around line 481-503: Update archive_skill to read the skill record from the
already-resolved store and return StatusCode::FORBIDDEN when its pinned flag is
true, before calling archive_skill_dir. Preserve the existing workspace-source
validation and archiving behavior for unpinned skills.
In `@src/main.rs`:
- Around line 1296-1298: Update the skill info usage-store flow around
open_skill_usage_store and store.get to handle both Err cases instead of
silently discarding them. Print a concise note explaining the usage section is
unavailable, including the relevant error, while preserving the existing output
for successful store opening and record retrieval.
- Around line 1348-1374: Open the usage store before moving the directory in
both SkillCommand::Archive and SkillCommand::Restore. Move the
open_skill_usage_store call ahead of archive_skill_dir or its restore
equivalent, while keeping the subsequent set_archived or restore state update
behavior unchanged.
In `@src/skills.rs`:
- Around line 403-483: Update archive_skill_dir and restore_skill_dir to
preserve the skill’s workspace-relative category path instead of using only name
under the workspace root. Archive into the corresponding relative location
beneath .archive, retain suffix collision handling, and restore to that same
relative workspace path while checking for an existing destination. Ensure
required category/archive directories are created and no category component is
dropped.
In `@src/skills/usage.rs`:
- Around line 256-266: Update SkillUsage::set_restored to upsert the skill_usage
record instead of relying on a bare UPDATE, creating a missing row with active
state and a null archived_at while preserving the existing restoration behavior
for rows that already exist. Reuse the lifecycle upsert pattern used by the
other methods in this file.
In `@src/tools/skill_manage.rs`:
- Around line 96-106: The poisoned SkillReadTracker mutex must be recoverable
instead of panicking. In src/tools/skill_manage.rs lines 96-106, update has_read
to recover the poisoned guard with into_inner; apply the same change in
src/tools/read_skill.rs lines 106-111 before inserting the lowercased skill
name, replacing the expect-based lock handling at both sites.
- Around line 143-148: Extend the read-before-write check in the skill
modification rail around action matching to include write_file and remove_file
alongside patch and edit. Preserve the existing has_read check and error
response so all mutations of skill content require a prior read_skill.
In `@src/tools/skills_list.rs`:
- Around line 68-81: Move the inline description from ToolDefinition::definition
into a new prompts/en/tools/skills_list_description.md.j2 file, preserving its
text. Register the tools/skills_list prompt key in src/prompts/text.rs and
update skills_list::definition to load the description through
crate::prompts::text::get, matching the existing skill_manage pattern.
---
Outside diff comments:
In `@src/tools.rs`:
- Around line 893-917: Review the branch tool server construction and decide the
intended write policy for SkillManageTool. If branch operations are autonomous,
change the SkillManageTool registration in the server built by the surrounding
function to use WriteOrigin::Agent, preserving the existing read tracker;
otherwise document or explicitly retain the User origin as an intentional policy
choice.
---
Nitpick comments:
In `@src/api/skills.rs`:
- Around line 537-542: Update the restore flow around restore_skill_dir so an
existing active skill directory is mapped to StatusCode::CONFLICT while an
absent archived copy remains StatusCode::NOT_FOUND. Distinguish these failure
cases using the returned error, preserve the warning context, and document both
404 and 409 responses in the associated utoipa::path definition.
In `@src/prompts/text.rs`:
- Around line 99-101: Move the ("en", "tools/skill_manage") match arm from its
current position between the fragment arms into the existing "Tool Descriptions"
block with the other tools/* entries, preserving its include_str! mapping
unchanged.
In `@src/skills.rs`:
- Around line 449-463: Replace the sequential suffix loop in the archive
source-selection logic with a single read of archive_root, then inspect matching
entries for name and name-N forms and select the highest numeric suffix.
Preserve the plain name as the base candidate and ensure gaps do not prevent
later archive copies from being selected.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6ea06518-bc85-401b-9cfb-7b2e3821dfca
📒 Files selected for processing (12)
prompts/en/tools/skill_manage_description.md.j2src/api/server.rssrc/api/skills.rssrc/config/types.rssrc/main.rssrc/prompts/text.rssrc/skills.rssrc/skills/usage.rssrc/tools.rssrc/tools/read_skill.rssrc/tools/skill_manage.rssrc/tools/skills_list.rs
- Skill tools register only for Default branch profile — ingestion and memory-persistence branches are autonomous pipelines and get no skill mutation surface. - Archive preserves the skill's path relative to the skills root, so categorized skills restore to their category; restore searches one category level deep. - Pin blocks archiving on the API endpoint and CLI, matching delete. - CLI archive/restore open the usage store before moving directories, so a store failure can't strand disk and DB state apart. - Read-before-write rail extended to write_file/remove_file. - Poisoned read-tracker lock recovers instead of panicking in tool calls. - set_restored upserts like the other lifecycle methods. - skill info reports usage-store errors instead of omitting the section. - skills_list description moved to prompts/ per repo convention.
|
On the outside-diff comment about WriteOrigin::User for the branch server: conversation branches keep User origin deliberately — they act inside a live conversation where the user is present and directing, which is the consent axis the origins encode (same reason Hermes lets foreground deletes remove but autonomous deletes archive). The real gap the comment surfaced was that ingestion and memory-persistence branches share this constructor and are fully autonomous: fixed in 19228fb by registering the skill tools only for the Default (conversation) branch profile, so pipeline branches get no skill mutation surface at all. The reflection branch lands in phase 3 with its own WriteOrigin::Agent server. |
Phase 2 of
docs/design-docs/skill-lifecycle.md: the mutation surface. Agents (and users, through the same tool) can now create, patch, and retire skills — with the write origin deciding what's allowed.skill_manage
One action-dispatched tool:
create,patch,edit,delete,write_file,remove_file.WriteOriginis set by the process constructing the tool server — the model never supplies it. Regular branches carryUserorigin; the Phase 3 reflection branch and Phase 4 curator will carryAgent, which activates the tighter rails:created_by = 'installed'), pinned, or instance-level ones.read_skill'd the target this session before patch/edit (a shared per-session read tracker between the two tools — a mechanical rail against patching imagined content).deletearchives to{workspace}/skills/.archive/(recoverable) instead of removing;Userdelete removes.deletewithabsorbed_intorequires the absorbing skill to exist on disk and differ from the target — consolidation fails closed on unverified destruction.Validation on all origins: name charset/length, frontmatter parses with a description within the 80-char budget, non-empty body, 100KB SKILL.md / 1MiB support file caps, and path rails on support files (must live under
references|templates|scripts|assets, no.., canonicalized inside the skill dir, symlinks refused). Every successful mutation reloads the liveSkillSetdirectly — no watcher reliance — and records toskill_usage(record_createdcarries conversation provenance for agent-created skills,record_patchbumps counters).skills_list
Companion read tool: every loaded skill with source, provenance, lifecycle state, pin flag, and usage counters — the picture the prompt index compresses away. Branches get both tools; workers keep
read_skillonly.Category discovery
createaccepts an optionalcategory, and the loader now scans one level deeper: bothskills/{name}/SKILL.mdandskills/{category}/{name}/SKILL.mdload.Lifecycle controls
POST /agents/skills/{pin,adopt,archive,restore}.adoptflipscreated_byto'agent'— the explicit act of handing a skill to curation; it is never automatic.spacebot skill pin|unpin|adopt|archive|restore, plus provenance/usage inspacebot skill info. The CLI opens only the agent's SQLite (not the full Db bundle), so it doesn't contend for the redb lock a running daemon holds.{name}-1,-2, ... suffixes); restore takes the most recent.Testing
15 tests on the tool: name/content validation, path escape + symlink refusal, and the full rail matrix exercised through real tool calls against a temp workspace and in-memory SQLite — user create/provenance, agent conversation-origin recording, read-before-write denial then success, installed/pinned refusals for agents, archive-vs-remove delete semantics, pinned user-delete block, absorbed_into fail-closed, support file write + linked_files reload, category discovery.