mirror of
https://github.com/block/buzz.git
synced 2026-11-04 00:12:02 +00:00
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.
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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": "<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.
|
||||
|
||||
## Parameter Conventions
|
||||
|
||||
@@ -154,7 +154,7 @@ sprout channels delete --channel <UUID>
|
||||
|
||||
## 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 <UUID>
|
||||
@@ -187,7 +187,7 @@ Get all reactions on an event:
|
||||
sprout reactions get --event <hex-event-id>
|
||||
```
|
||||
|
||||
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 |
|
||||
|
||||
Reference in New Issue
Block a user