Skip to content

Commit 59513f8

Browse files
committed
fix(nest): correct skill content inaccuracies + chmod intermediate dirs
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.
1 parent d13ffc2 commit 59513f8

2 files changed

Lines changed: 33 additions & 18 deletions

File tree

desktop/src-tauri/src/managed_agents/nest.rs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ pub fn ensure_nest() -> Result<(), String> {
4747
///
4848
/// - Creates the root directory and all subdirectories.
4949
/// - Writes `AGENTS.md` only if it doesn't already exist.
50-
/// - Sets 700 permissions on the root and all subdirectories (Unix).
50+
/// - Writes `.claude/skills/sprout-cli/SKILL.md` only if it doesn't already exist.
51+
/// - Sets 700 permissions on the root, all subdirectories, and the skill
52+
/// directory tree (Unix).
5153
///
5254
/// Idempotent: safe to call on every launch. Existing files are never
53-
/// overwritten — users can freely edit AGENTS.md and it will persist.
55+
/// overwritten — users can freely edit AGENTS.md or SKILL.md and they persist.
5456
///
5557
/// Rejects symlinks at the root path to prevent redirect attacks.
5658
///
@@ -143,14 +145,22 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> {
143145
.map_err(|e| format!("set permissions on {}: {e}", path.display()))?;
144146
}
145147
}
146-
// Skill directory also gets 700 permissions.
147-
let is_symlink = skill_dir
148-
.symlink_metadata()
149-
.map(|m| m.file_type().is_symlink())
150-
.unwrap_or(false);
151-
if !is_symlink {
152-
fs::set_permissions(&skill_dir, perms.clone())
153-
.map_err(|e| format!("set permissions on {}: {e}", skill_dir.display()))?;
148+
// Skill directory and its intermediate parents inside root get 700.
149+
// create_dir_all creates .claude/ and .claude/skills/ with umask
150+
// defaults — lock them down the same way we do NEST_DIRS.
151+
for dir in [
152+
root.join(".claude"),
153+
root.join(".claude/skills"),
154+
skill_dir.clone(),
155+
] {
156+
let is_symlink = dir
157+
.symlink_metadata()
158+
.map(|m| m.file_type().is_symlink())
159+
.unwrap_or(false);
160+
if !is_symlink {
161+
fs::set_permissions(&dir, perms.clone())
162+
.map_err(|e| format!("set permissions on {}: {e}", dir.display()))?;
163+
}
154164
}
155165
}
156166

@@ -269,9 +279,12 @@ mod tests {
269279
let tmp = tempfile::tempdir().unwrap();
270280
let root = tmp.path().join(".sprout");
271281
ensure_nest_at(&root).unwrap();
272-
let skill_dir = root.join(".claude/skills/sprout-cli");
273-
let mode = fs::metadata(&skill_dir).unwrap().permissions().mode() & 0o777;
274-
assert_eq!(mode, 0o700, "skill dir should be 700");
282+
// All three dirs in the skill path should be locked down.
283+
for dir in [".claude", ".claude/skills", ".claude/skills/sprout-cli"] {
284+
let path = root.join(dir);
285+
let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
286+
assert_eq!(mode, 0o700, "{dir} should be 700");
287+
}
275288
}
276289

277290
#[cfg(unix)]

desktop/src-tauri/src/managed_agents/nest_skill.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ version: 1
2121

2222
All output is JSON on stdout. Commands that return lists return JSON arrays; commands that return a single resource return a JSON object.
2323

24-
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.
24+
Errors go to stderr as `{"error": "<category>", "message": "<detail>"}`. 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.
2525

2626
## Parameter Conventions
2727

@@ -154,7 +154,7 @@ sprout channels delete --channel <UUID>
154154

155155
## Canvas
156156

157-
Get the canvas document for a channel (returns the markdown content string directly, not JSON-wrapped):
157+
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):
158158

159159
```bash
160160
sprout canvas get --channel <UUID>
@@ -187,7 +187,7 @@ Get all reactions on an event:
187187
sprout reactions get --event <hex-event-id>
188188
```
189189

190-
Returns `{"reactions": [{emoji, count, pubkeys}]}`.
190+
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.
191191

192192
## DMs
193193

@@ -263,6 +263,8 @@ sprout users set-presence --status away
263263
sprout users set-presence --status offline
264264
```
265265

266+
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.
267+
266268
## Workflows
267269

268270
List workflows for a channel:
@@ -353,10 +355,10 @@ Use shorter intervals (10s) when latency matters; longer intervals (30s) for bac
353355
| `channels create` | `--name`, `--type`, `--visibility` | `{event_id, channel_id, accepted, message}` |
354356
| `channels join` | `--channel` | `{event_id, accepted, message}` |
355357
| `channels members` | `--channel` | `[{pubkey, role}]` |
356-
| `canvas get` | `--channel` | markdown string (not JSON) |
358+
| `canvas get` | `--channel` | JSON array of kind:40100 events (markdown in `content`) |
357359
| `canvas set` | `--channel`, `--content` | `{event_id, accepted, message}` |
358360
| `reactions add` | `--event`, `--emoji` | `{event_id, accepted, message}` |
359-
| `reactions get` | `--event` | `{"reactions": [{emoji, count, pubkeys}]}` |
361+
| `reactions get` | `--event` | JSON array of kind:7 reaction events |
360362
| `dms list` || `[{dm_id, participants, created_at}]` |
361363
| `dms open` | `--pubkey` | `{event_id, dm_id, accepted, message}` |
362364
| `users get` || flat profile object |

0 commit comments

Comments
 (0)