From 8339e5f832c9ba861e6abd379839163fa021fb8d Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Mon, 18 May 2026 19:08:26 -0400 Subject: [PATCH] fix(nest): correct skill content inaccuracies + chmod intermediate dirs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four inaccuracies in nest_skill.md caught by comparing against CLI source: 1. canvas get returns a JSON array of kind:40100 events (markdown in `content`), not a plain markdown string. Agents parsing this as a raw string would get the Nostr event wrapper, not the content. 2. reactions get returns a raw JSON array of kind:7 events, not the aggregated {"reactions": [...]} shape. No aggregation happens in cmd_get_reactions. 3. Error stderr category field values were placeholder text ("input error", "network/relay error") instead of the actual enum values emitted by error.rs: user_error, relay_error, network_error, auth_error, key_error. 4. set-presence documents as working but cmd_set_presence has an explicit comment noting it always fails — kind:20001 is ephemeral and the relay rejects it over HTTP POST. Added a "this will fail" caveat. Also fixes a permission gap in nest.rs: create_dir_all(".claude/skills/ sprout-cli") created the intermediate .claude/ and .claude/skills/ dirs with umask-default permissions (typically 755), but only the final sprout-cli/ dir was chmod'd to 700. Lock down all three dirs in the skill path, matching how NEST_DIRS are handled. Test updated to assert all three dirs get 700 on Unix. Docstring for ensure_nest_at updated to mention SKILL.md. --- desktop/src-tauri/src/managed_agents/nest.rs | 39 ++++++++++++------- .../src/managed_agents/nest_skill.md | 12 +++--- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index e2d7ab9173..a84ebeb7bc 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -47,10 +47,12 @@ pub fn ensure_nest() -> Result<(), String> { /// /// - Creates the root directory and all subdirectories. /// - Writes `AGENTS.md` only if it doesn't already exist. -/// - Sets 700 permissions on the root and all subdirectories (Unix). +/// - Writes `.claude/skills/sprout-cli/SKILL.md` only if it doesn't already exist. +/// - Sets 700 permissions on the root, all subdirectories, and the skill +/// directory tree (Unix). /// /// Idempotent: safe to call on every launch. Existing files are never -/// overwritten — users can freely edit AGENTS.md and it will persist. +/// overwritten — users can freely edit AGENTS.md or SKILL.md and they persist. /// /// Rejects symlinks at the root path to prevent redirect attacks. /// @@ -143,14 +145,22 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> { .map_err(|e| format!("set permissions on {}: {e}", path.display()))?; } } - // Skill directory also gets 700 permissions. - let is_symlink = skill_dir - .symlink_metadata() - .map(|m| m.file_type().is_symlink()) - .unwrap_or(false); - if !is_symlink { - fs::set_permissions(&skill_dir, perms.clone()) - .map_err(|e| format!("set permissions on {}: {e}", skill_dir.display()))?; + // Skill directory and its intermediate parents inside root get 700. + // create_dir_all creates .claude/ and .claude/skills/ with umask + // defaults — lock them down the same way we do NEST_DIRS. + for dir in [ + root.join(".claude"), + root.join(".claude/skills"), + skill_dir.clone(), + ] { + let is_symlink = dir + .symlink_metadata() + .map(|m| m.file_type().is_symlink()) + .unwrap_or(false); + if !is_symlink { + fs::set_permissions(&dir, perms.clone()) + .map_err(|e| format!("set permissions on {}: {e}", dir.display()))?; + } } } @@ -327,9 +337,12 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path().join(".sprout"); ensure_nest_at(&root).unwrap(); - let skill_dir = root.join(".claude/skills/sprout-cli"); - let mode = fs::metadata(&skill_dir).unwrap().permissions().mode() & 0o777; - assert_eq!(mode, 0o700, "skill dir should be 700"); + // All three dirs in the skill path should be locked down. + for dir in [".claude", ".claude/skills", ".claude/skills/sprout-cli"] { + let path = root.join(dir); + let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777; + assert_eq!(mode, 0o700, "{dir} should be 700"); + } } #[cfg(unix)] diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md index 9499dbc73f..a230c51da8 100644 --- a/desktop/src-tauri/src/managed_agents/nest_skill.md +++ b/desktop/src-tauri/src/managed_agents/nest_skill.md @@ -21,7 +21,7 @@ version: 1 All output is JSON on stdout. Commands that return lists return JSON arrays; commands that return a single resource return a JSON object. -Errors go to stderr as `{"error": "category", "message": "detail"}`. Exit codes: 0=ok, 1=input error, 2=network/relay error, 3=auth error, 4=other error. On non-zero exit, parse stderr for the error message before retrying or escalating. +Errors go to stderr as `{"error": "", "message": ""}`. Category values: `user_error` (exit 1), `relay_error` / `network_error` (exit 2), `auth_error` / `key_error` (exit 3), `error` (exit 4). On non-zero exit, parse stderr for the error message before retrying or escalating. ## Parameter Conventions @@ -154,7 +154,7 @@ sprout channels delete --channel ## Canvas -Get the canvas document for a channel (returns the markdown content string directly, not JSON-wrapped): +Get the canvas document for a channel (returns a JSON array of kind:40100 events; the canvas markdown is in the `content` field of the first element): ```bash sprout canvas get --channel @@ -187,7 +187,7 @@ Get all reactions on an event: sprout reactions get --event ``` -Returns `{"reactions": [{emoji, count, pubkeys}]}`. +Returns a JSON array of raw kind:7 reaction events. Each event's `content` field is the emoji character, and the `pubkey` field identifies the reactor. ## DMs @@ -263,6 +263,8 @@ sprout users set-presence --status away sprout users set-presence --status offline ``` +Note: `set-presence` sends an ephemeral kind:20001 event that requires a WebSocket connection. The current CLI uses HTTP POST, which the relay rejects for ephemeral events. This command will fail until WebSocket support is added. + ## Workflows List workflows for a channel: @@ -353,10 +355,10 @@ Use shorter intervals (10s) when latency matters; longer intervals (30s) for bac | `channels create` | `--name`, `--type`, `--visibility` | `{event_id, channel_id, accepted, message}` | | `channels join` | `--channel` | `{event_id, accepted, message}` | | `channels members` | `--channel` | `[{pubkey, role}]` | -| `canvas get` | `--channel` | markdown string (not JSON) | +| `canvas get` | `--channel` | JSON array of kind:40100 events (markdown in `content`) | | `canvas set` | `--channel`, `--content` | `{event_id, accepted, message}` | | `reactions add` | `--event`, `--emoji` | `{event_id, accepted, message}` | -| `reactions get` | `--event` | `{"reactions": [{emoji, count, pubkeys}]}` | +| `reactions get` | `--event` | JSON array of kind:7 reaction events | | `dms list` | — | `[{dm_id, participants, created_at}]` | | `dms open` | `--pubkey` | `{event_id, dm_id, accepted, message}` | | `users get` | — | flat profile object |