@bookedsolid/reagent 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: symlink-guard.sh
3
+ # Fires BEFORE every Write tool call.
4
+ # Resolves the target file path and blocks if it escapes the project root.
5
+ # Guards against symlink traversal attacks where a path resolves outside the repo.
6
+ #
7
+ # Content extraction:
8
+ # Write tool → tool_input.file_path
9
+ #
10
+ # Exit codes:
11
+ # 0 = path is within project root — allow
12
+ # 2 = path escapes project root — block
13
+
14
+ set -uo pipefail
15
+
16
+ INPUT=$(cat)
17
+
18
+ # ── Dependency check ──────────────────────────────────────────────────────────
19
+ if ! command -v jq >/dev/null 2>&1; then
20
+ printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
21
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
22
+ exit 2
23
+ fi
24
+
25
+ # ── HALT check ────────────────────────────────────────────────────────────────
26
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
27
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
28
+ if [ -f "$HALT_FILE" ]; then
29
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
30
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
31
+ exit 2
32
+ fi
33
+
34
+ TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
35
+
36
+ # Only applies to Write tool
37
+ if [[ "$TOOL_NAME" != "Write" ]]; then
38
+ exit 0
39
+ fi
40
+
41
+ FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
42
+
43
+ if [[ -z "$FILE_PATH" ]]; then
44
+ exit 0
45
+ fi
46
+
47
+ # ── Determine project root ────────────────────────────────────────────────────
48
+ # Walk up from CLAUDE_PROJECT_DIR looking for .claude/ directory
49
+ PROJECT_ROOT=""
50
+ SEARCH_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
51
+ while [[ "$SEARCH_DIR" != "/" ]]; do
52
+ if [[ -d "$SEARCH_DIR/.claude" ]] || [[ -d "$SEARCH_DIR/.reagent" ]]; then
53
+ PROJECT_ROOT="$SEARCH_DIR"
54
+ break
55
+ fi
56
+ SEARCH_DIR=$(dirname "$SEARCH_DIR")
57
+ done
58
+
59
+ if [[ -z "$PROJECT_ROOT" ]]; then
60
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
61
+ fi
62
+
63
+ # ── Resolve the file path (no-symlinks) ───────────────────────────────────────
64
+ # Use python3 for portable path resolution since realpath --no-symlinks
65
+ # is not universally available (macOS ships an older realpath)
66
+ if command -v python3 >/dev/null 2>&1; then
67
+ RESOLVED=$(python3 -c "
68
+ import os, sys
69
+ p = sys.argv[1]
70
+ # os.path.realpath resolves symlinks; we use normpath for lexical-only resolution
71
+ # to detect path traversal without requiring the path to exist
72
+ print(os.path.normpath(os.path.abspath(p)))
73
+ " "$FILE_PATH" 2>/dev/null)
74
+ elif command -v realpath >/dev/null 2>&1; then
75
+ # Fallback: realpath without --canonicalize-missing may still work on some systems
76
+ RESOLVED=$(realpath -m "$FILE_PATH" 2>/dev/null || realpath "$FILE_PATH" 2>/dev/null || printf '%s' "$FILE_PATH")
77
+ else
78
+ # Last resort: use pwd-relative normalization
79
+ RESOLVED=$(cd "$(dirname "$FILE_PATH")" 2>/dev/null && pwd)/$(basename "$FILE_PATH") || printf '%s' "$FILE_PATH"
80
+ fi
81
+
82
+ # ── Check if resolved path is within project root ─────────────────────────────
83
+ RESOLVED_PROJECT=$(python3 -c "import os; print(os.path.normpath(os.path.abspath('$PROJECT_ROOT')))" 2>/dev/null || printf '%s' "$PROJECT_ROOT")
84
+
85
+ # Path must start with project root followed by / or be exactly the root
86
+ if [[ "$RESOLVED" != "$RESOLVED_PROJECT"/* ]] && [[ "$RESOLVED" != "$RESOLVED_PROJECT" ]]; then
87
+ printf 'SYMLINK-GUARD: Path escapes project root — blocked\n' >&2
88
+ printf ' Requested path: %s\n' "$FILE_PATH" >&2
89
+ printf ' Resolved path: %s\n' "$RESOLVED" >&2
90
+ printf ' Project root: %s\n' "$RESOLVED_PROJECT" >&2
91
+ printf 'Block reason: The resolved path is outside the project directory.\n' >&2
92
+ printf 'This may indicate a symlink traversal attempt or an incorrect absolute path.\n' >&2
93
+ exit 2
94
+ fi
95
+
96
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/reagent",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Zero-trust MCP gateway — policy enforcement, secret redaction, and audit logging for AI-assisted projects",
5
5
  "license": "MIT",
6
6
  "author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",