@aporthq/aport-agent-guardrails 1.0.13 → 1.0.14

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.
@@ -109,6 +109,64 @@ DEFAULT_AGENT_NAME=${DEFAULT_AGENT_NAME:-"OpenClaw Agent"}
109
109
  DEFAULT_AGENT_DESC=$(get_identity_description) || true
110
110
  DEFAULT_AGENT_DESC=${DEFAULT_AGENT_DESC:-"Local OpenClaw AI agent with APort guardrails"}
111
111
 
112
+ # Framework-aware defaults: each framework needs different capabilities to function
113
+ case "${APORT_FRAMEWORK:-}" in
114
+ claude-code)
115
+ # Claude Code: file ops, web, sub-agents, MCP tools are core functionality
116
+ DEFAULT_FILE_READ=y
117
+ DEFAULT_FILE_WRITE=y
118
+ DEFAULT_WEB_FETCH=y
119
+ DEFAULT_WEB_BROWSER=y
120
+ DEFAULT_AGENT_SESSION=y
121
+ DEFAULT_MCP_TOOL=y
122
+ ;;
123
+ cursor)
124
+ # Cursor: IDE with shell, file ops, web search, agent mode
125
+ DEFAULT_FILE_READ=y
126
+ DEFAULT_FILE_WRITE=y
127
+ DEFAULT_WEB_FETCH=y
128
+ DEFAULT_WEB_BROWSER=n
129
+ DEFAULT_AGENT_SESSION=y
130
+ DEFAULT_MCP_TOOL=n
131
+ ;;
132
+ crewai)
133
+ # CrewAI: multi-agent framework — agents spawn sub-agents, use tools
134
+ DEFAULT_FILE_READ=y
135
+ DEFAULT_FILE_WRITE=y
136
+ DEFAULT_WEB_FETCH=y
137
+ DEFAULT_WEB_BROWSER=n
138
+ DEFAULT_AGENT_SESSION=y
139
+ DEFAULT_MCP_TOOL=y
140
+ ;;
141
+ langchain)
142
+ # LangChain/LangGraph: tool calling, web, files
143
+ DEFAULT_FILE_READ=y
144
+ DEFAULT_FILE_WRITE=y
145
+ DEFAULT_WEB_FETCH=y
146
+ DEFAULT_WEB_BROWSER=n
147
+ DEFAULT_AGENT_SESSION=n
148
+ DEFAULT_MCP_TOOL=n
149
+ ;;
150
+ n8n)
151
+ # n8n: workflow automation — HTTP, file, database, messaging
152
+ DEFAULT_FILE_READ=y
153
+ DEFAULT_FILE_WRITE=y
154
+ DEFAULT_WEB_FETCH=y
155
+ DEFAULT_WEB_BROWSER=n
156
+ DEFAULT_AGENT_SESSION=n
157
+ DEFAULT_MCP_TOOL=n
158
+ ;;
159
+ *)
160
+ # Generic / unknown framework: conservative defaults
161
+ DEFAULT_FILE_READ=n
162
+ DEFAULT_FILE_WRITE=n
163
+ DEFAULT_WEB_FETCH=n
164
+ DEFAULT_WEB_BROWSER=n
165
+ DEFAULT_AGENT_SESSION=n
166
+ DEFAULT_MCP_TOOL=n
167
+ ;;
168
+ esac
169
+
112
170
  if [ -n "$NON_INTERACTIVE" ]; then
113
171
  # CI/tests: use defaults, no prompts. Use --output or APORT_FRAMEWORK for default path. Match interactive defaults (README: messaging out of the box).
114
172
  owner_id="$DEFAULT_EMAIL"
@@ -119,10 +177,12 @@ if [ -n "$NON_INTERACTIVE" ]; then
119
177
  exec_cap=y
120
178
  msg_cap=y
121
179
  data_cap=n
122
- file_read_cap=n
123
- file_write_cap=n
124
- web_fetch_cap=n
125
- web_browser_cap=n
180
+ file_read_cap=$DEFAULT_FILE_READ
181
+ file_write_cap=$DEFAULT_FILE_WRITE
182
+ web_fetch_cap=$DEFAULT_WEB_FETCH
183
+ web_browser_cap=$DEFAULT_WEB_BROWSER
184
+ agent_session_cap=$DEFAULT_AGENT_SESSION
185
+ mcp_tool_cap=$DEFAULT_MCP_TOOL
126
186
  max_pr_size=500
127
187
  max_prs_per_day=10
128
188
  max_msgs_per_day=100
@@ -195,7 +255,11 @@ else
195
255
  # Choose capabilities
196
256
  echo " 🔐 Capabilities"
197
257
  echo " ───────────────"
198
- echo " Choose what your agent can do (y/n). Defaults: PRs, exec, and messaging = yes (matches README/docs); others = no."
258
+ if [ "${APORT_FRAMEWORK:-}" = "claude-code" ]; then
259
+ echo " Choose what your agent can do (y/n). Claude Code defaults: most capabilities = yes."
260
+ else
261
+ echo " Choose what your agent can do (y/n). Defaults: PRs, exec, and messaging = yes (matches README/docs); others = no."
262
+ fi
199
263
  echo ""
200
264
  read -p " • Create and merge pull requests? [Y/n]: " pr_cap
201
265
  pr_cap=${pr_cap:-y}
@@ -206,21 +270,51 @@ else
206
270
  read -p " • Send messages (email, SMS, etc.)? [Y/n]: " msg_cap
207
271
  msg_cap=${msg_cap:-y}
208
272
 
209
- read -p " Read files from disk? [y/N]: " file_read_cap
210
- file_read_cap=${file_read_cap:-n}
273
+ if [ "$DEFAULT_FILE_READ" = "y" ]; then
274
+ read -p " • Read files from disk? [Y/n]: " file_read_cap
275
+ else
276
+ read -p " • Read files from disk? [y/N]: " file_read_cap
277
+ fi
278
+ file_read_cap=${file_read_cap:-$DEFAULT_FILE_READ}
211
279
 
212
- read -p " Write/edit files on disk? [y/N]: " file_write_cap
213
- file_write_cap=${file_write_cap:-n}
280
+ if [ "$DEFAULT_FILE_WRITE" = "y" ]; then
281
+ read -p " • Write/edit files on disk? [Y/n]: " file_write_cap
282
+ else
283
+ read -p " • Write/edit files on disk? [y/N]: " file_write_cap
284
+ fi
285
+ file_write_cap=${file_write_cap:-$DEFAULT_FILE_WRITE}
214
286
 
215
- read -p " Fetch data from web (HTTP requests)? [y/N]: " web_fetch_cap
216
- web_fetch_cap=${web_fetch_cap:-n}
287
+ if [ "$DEFAULT_WEB_FETCH" = "y" ]; then
288
+ read -p " • Fetch data from web (HTTP requests)? [Y/n]: " web_fetch_cap
289
+ else
290
+ read -p " • Fetch data from web (HTTP requests)? [y/N]: " web_fetch_cap
291
+ fi
292
+ web_fetch_cap=${web_fetch_cap:-$DEFAULT_WEB_FETCH}
217
293
 
218
- read -p " Automate web browser? [y/N]: " web_browser_cap
219
- web_browser_cap=${web_browser_cap:-n}
294
+ if [ "$DEFAULT_WEB_BROWSER" = "y" ]; then
295
+ read -p " • Automate web browser? [Y/n]: " web_browser_cap
296
+ else
297
+ read -p " • Automate web browser? [y/N]: " web_browser_cap
298
+ fi
299
+ web_browser_cap=${web_browser_cap:-$DEFAULT_WEB_BROWSER}
220
300
 
221
301
  read -p " • Export data (database, files, etc.)? [y/N]: " data_cap
222
302
  data_cap=${data_cap:-n}
223
303
 
304
+ if [ "$DEFAULT_AGENT_SESSION" = "y" ]; then
305
+ read -p " • Spawn sub-agents and tasks? [Y/n]: " agent_session_cap
306
+ else
307
+ read -p " • Spawn sub-agents and tasks? [y/N]: " agent_session_cap
308
+ fi
309
+ agent_session_cap=${agent_session_cap:-$DEFAULT_AGENT_SESSION}
310
+
311
+ if [ "$DEFAULT_MCP_TOOL" = "y" ]; then
312
+ read -p " • Use MCP tools (external integrations)? [Y/n]: " mcp_tool_cap
313
+ else
314
+ read -p " • Use MCP tools (external integrations)? [y/N]: " mcp_tool_cap
315
+ fi
316
+ mcp_tool_cap=${mcp_tool_cap:-$DEFAULT_MCP_TOOL}
317
+
224
318
  echo ""
225
319
 
226
320
  # Configure limits
@@ -351,6 +445,12 @@ fi
351
445
  if [ "$data_cap" = "y" ] || [ "$data_cap" = "Y" ]; then
352
446
  capabilities_json="$capabilities_json{\"id\": \"data.export\"},"
353
447
  fi
448
+ if [ "${agent_session_cap:-n}" = "y" ] || [ "${agent_session_cap:-n}" = "Y" ]; then
449
+ capabilities_json="$capabilities_json{\"id\": \"agent.session.create\"},"
450
+ fi
451
+ if [ "${mcp_tool_cap:-n}" = "y" ] || [ "${mcp_tool_cap:-n}" = "Y" ]; then
452
+ capabilities_json="$capabilities_json{\"id\": \"mcp.tool.execute\"},"
453
+ fi
354
454
  # Remove trailing comma
355
455
  capabilities_json="${capabilities_json%,}]"
356
456
 
@@ -438,6 +538,14 @@ if [ "$data_cap" = "y" ] || [ "$data_cap" = "Y" ]; then
438
538
  limits_json="$limits_json\"data.export\": {\"max_rows\": $max_export_rows, \"allow_pii\": $allow_pii_bool, \"allowed_collections\": [\"*\"]},"
439
539
  fi
440
540
 
541
+ if [ "${agent_session_cap:-n}" = "y" ] || [ "${agent_session_cap:-n}" = "Y" ]; then
542
+ limits_json="$limits_json\"agent.session.create\": {\"max_concurrent\": 10},"
543
+ fi
544
+
545
+ if [ "${mcp_tool_cap:-n}" = "y" ] || [ "${mcp_tool_cap:-n}" = "Y" ]; then
546
+ limits_json="$limits_json\"mcp.tool.execute\": {\"allowed_servers\": [\"*\"]},"
547
+ fi
548
+
441
549
  # Remove trailing comma
442
550
  limits_json="${limits_json%,}}"
443
551
 
@@ -540,7 +648,13 @@ echo " 🔐 Capabilities:"
540
648
  [ "$pr_cap" = "y" ] || [ "$pr_cap" = "Y" ] && echo " • Create and merge pull requests"
541
649
  [ "$exec_cap" = "y" ] || [ "$exec_cap" = "Y" ] && echo " • Execute system commands"
542
650
  [ "$msg_cap" = "y" ] || [ "$msg_cap" = "Y" ] && echo " • Send messages"
651
+ [ "$file_read_cap" = "y" ] || [ "$file_read_cap" = "Y" ] && echo " • Read files"
652
+ [ "$file_write_cap" = "y" ] || [ "$file_write_cap" = "Y" ] && echo " • Write/edit files"
653
+ [ "$web_fetch_cap" = "y" ] || [ "$web_fetch_cap" = "Y" ] && echo " • Fetch from web"
654
+ [ "$web_browser_cap" = "y" ] || [ "$web_browser_cap" = "Y" ] && echo " • Automate browser"
543
655
  [ "$data_cap" = "y" ] || [ "$data_cap" = "Y" ] && echo " • Export data"
656
+ [ "${agent_session_cap:-n}" = "y" ] || [ "${agent_session_cap:-n}" = "Y" ] && echo " • Spawn sub-agents and tasks"
657
+ [ "${mcp_tool_cap:-n}" = "y" ] || [ "${mcp_tool_cap:-n}" = "Y" ] && echo " • Use MCP tools"
544
658
  echo ""
545
659
  echo " 📝 Next steps:"
546
660
  echo " • Review limits: vim $PASSPORT_FILE"
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env bash
2
- # APort Cursor/Copilot/Claude Code hook: read JSON from stdin, call guardrail, return allow/deny; exit 2 = block.
3
- # Compatible with Cursor (beforeShellExecution, preToolUse), VS Code Copilot, and Claude Code.
4
- # Input: JSON with "command" and/or "tool"/"name"/"input" (host-dependent). We map to system.command.execute.
5
- # Output: JSON with "permission": "allow"|"deny" (Cursor) and "allowed": true|false; optional "agentMessage"/"reason".
6
- # Exit: 0 = allow, 2 = block (deny). Other exits = hook error (host may proceed or fail-open).
2
+ # APort Cursor hook: reads JSON from stdin, maps tool to APort policy, calls guardrail.
3
+ # Handles all Cursor hook events: beforeShellExecution, preToolUse, beforeMCPExecution,
4
+ # beforeReadFile, subagentStart.
5
+ # Output: JSON with "permission": "allow"|"deny"; optional "agentMessage"/"user_message".
6
+ # Exit: 0 = allow, 2 = block (deny). Other exits = hook error (Cursor may fail-open).
7
+ #
8
+ # Cursor preToolUse tool_name values: Shell, Read, Write, Grep, Delete, Task, MCP:<name>
9
+ # See: https://cursor.com/docs/hooks
7
10
 
8
11
  set -e
9
12
 
@@ -11,14 +14,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
14
  ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
15
  GUARDRAIL="$ROOT_DIR/bin/aport-guardrail-bash.sh"
13
16
 
14
- # Passport/config: resolver probes ~/.cursor, ~/.openclaw, ~/.aport/langchain, etc. when OPENCLAW_CONFIG_DIR not set
17
+ # Passport/config: resolver probes ~/.cursor, ~/.openclaw, ~/.aport/*, etc.
15
18
  # shellcheck source=bin/aport-resolve-paths.sh
16
19
  . "$ROOT_DIR/bin/aport-resolve-paths.sh"
17
20
 
18
21
  # Read stdin (single JSON object; Cursor sends one payload per invocation)
19
22
  INPUT=""
20
23
  if [ -t 0 ]; then
21
- # No stdin (e.g. manual test): treat as allow to avoid blocking
22
24
  INPUT='{}'
23
25
  else
24
26
  INPUT="$(cat)"
@@ -30,61 +32,145 @@ if [ -z "$INPUT" ]; then
30
32
  exit 0
31
33
  fi
32
34
 
33
- # Parse and normalize to tool + context for guardrail
34
- # Cursor beforeShellExecution: { "command": "..." }
35
- # preToolUse / Copilot: { "tool": "runTerminalCommand", "input": { "command": "..." } } or similar
36
- TOOL_NAME="exec.run"
35
+ # Require jq for JSON parsing
36
+ if ! command -v jq &> /dev/null; then
37
+ echo '{"permission":"deny","allowed":false,"agentMessage":"APort: jq is required"}'
38
+ exit 2
39
+ fi
40
+
41
+ # Deny helper: outputs Cursor-format JSON and exits 2
42
+ deny() {
43
+ local reason="$1"
44
+ jq -n -c --arg reason "$reason" \
45
+ '{permission:"deny",allowed:false,agentMessage:$reason,reason:$reason}'
46
+ exit 2
47
+ }
48
+
49
+ # Safe jq extraction: returns '{}' on any jq error
50
+ safe_jq() {
51
+ local input="$1" filter="$2"
52
+ local result
53
+ result="$(echo "$input" | jq -c "$filter" 2> /dev/null)" || result='{}'
54
+ [ -z "$result" ] && result='{}'
55
+ echo "$result"
56
+ }
57
+
58
+ # Detect hook event type from input fields and route accordingly.
59
+ # Cursor sends different JSON shapes per hook event:
60
+ # beforeShellExecution: { "command": "...", "cwd": "..." }
61
+ # preToolUse: { "tool_name": "Shell|Read|Write|...", "tool_input": {...} }
62
+ # beforeMCPExecution: { "tool_name": "...", "tool_input": {...}, "server": "..." }
63
+ # beforeReadFile: { "file_path": "...", "content": "..." }
64
+ # subagentStart: { "subagent_id": "...", "subagent_type": "...", "task": "..." }
65
+ # We detect by checking for distinguishing fields.
66
+
67
+ GUARDRAIL_TOOL=""
37
68
  CONTEXT_JSON="{}"
38
- if command -v jq &> /dev/null; then
39
- CMD=$(echo "$INPUT" | jq -r '.command // .input.command // .input.cmd // .args[0] // ""')
40
- if [ -n "$CMD" ] && [ "$CMD" != "null" ]; then
41
- CONTEXT_JSON=$(echo "$INPUT" | jq -c '{command: (.command // .input.command // .input.cmd), args: (.args // .input.args // [])}' 2> /dev/null || echo "{}")
42
- if [ -z "$CONTEXT_JSON" ] || [ "$CONTEXT_JSON" = "null" ]; then
43
- CONTEXT_JSON=$(jq -n -c --arg cmd "$CMD" '{command: $cmd}')
44
- fi
45
- fi
46
- # If no command found, still pass through; guardrail may deny unknown
47
- if [ -z "$CONTEXT_JSON" ] || [ "$CONTEXT_JSON" = "null" ]; then
48
- CONTEXT_JSON="$INPUT"
49
- fi
69
+
70
+ # Check for hook_event_name first (newer Cursor versions include it)
71
+ HOOK_EVENT="$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2> /dev/null)"
72
+ TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // ""' 2> /dev/null)"
73
+
74
+ if [ "$HOOK_EVENT" = "beforeReadFile" ] || { [ -z "$HOOK_EVENT" ] && [ -z "$TOOL_NAME" ] && echo "$INPUT" | jq -e '.file_path and .content' &> /dev/null; }; then
75
+ # beforeReadFile: file reads — allow without evaluator (same as Claude Code)
76
+ exit 0
77
+
78
+ elif [ "$HOOK_EVENT" = "subagentStart" ] || { [ -z "$HOOK_EVENT" ] && echo "$INPUT" | jq -e '.subagent_id' &> /dev/null; }; then
79
+ # subagentStart: sub-agent spawning
80
+ GUARDRAIL_TOOL="session.create"
81
+ CONTEXT_JSON="$(safe_jq "$INPUT" '{description: (.task // ""), subagent_type: (.subagent_type // "")}')"
82
+
83
+ elif [ "$HOOK_EVENT" = "beforeMCPExecution" ] || { [ -n "$TOOL_NAME" ] && echo "$INPUT" | jq -e '.server // .url' &> /dev/null; }; then
84
+ # beforeMCPExecution: MCP tool calls (has server/url field)
85
+ GUARDRAIL_TOOL="mcp.tool"
86
+ CONTEXT_JSON="$(safe_jq "$INPUT" '{tool_name: (.tool_name // ""), tool_input: (.tool_input // {})}')"
87
+
88
+ elif [ -n "$TOOL_NAME" ]; then
89
+ # preToolUse: Cursor tool names — Shell, Read, Write, Grep, Delete, Task, MCP:<name>
90
+ case "$TOOL_NAME" in
91
+ Shell)
92
+ GUARDRAIL_TOOL="bash"
93
+ CONTEXT_JSON="$(safe_jq "$INPUT" '{command: (.tool_input.command // "")}')"
94
+ ;;
95
+ Read | Grep)
96
+ # Read-family: allow without calling evaluator
97
+ exit 0
98
+ ;;
99
+ Write)
100
+ GUARDRAIL_TOOL="write"
101
+ CONTEXT_JSON="$(safe_jq "$INPUT" '{file_path: (.tool_input.file_path // .tool_input.path // "")}')"
102
+ ;;
103
+ Delete)
104
+ GUARDRAIL_TOOL="write"
105
+ CONTEXT_JSON="$(safe_jq "$INPUT" '{file_path: (.tool_input.file_path // .tool_input.path // "")}')"
106
+ ;;
107
+ Task)
108
+ GUARDRAIL_TOOL="session.create"
109
+ CONTEXT_JSON="$(safe_jq "$INPUT" '{description: (.tool_input.description // .tool_input.prompt // "")}')"
110
+ ;;
111
+ MCP:*)
112
+ GUARDRAIL_TOOL="mcp.tool"
113
+ CONTEXT_JSON="$(safe_jq "$INPUT" '{tool_name: (.tool_name // ""), tool_input: (.tool_input // {})}')"
114
+ ;;
115
+ *)
116
+ # Unknown preToolUse tool: fail-closed
117
+ deny "🛡️ APort: unknown tool '$TOOL_NAME' — fail-closed policy"
118
+ ;;
119
+ esac
120
+
121
+ elif echo "$INPUT" | jq -e '.command' &> /dev/null; then
122
+ # beforeShellExecution: { "command": "..." }
123
+ GUARDRAIL_TOOL="bash"
124
+ CMD="$(echo "$INPUT" | jq -r '.command // ""' 2> /dev/null)"
125
+ CONTEXT_JSON="$(jq -n -c --arg cmd "$CMD" '{command: $cmd}')"
126
+
127
+ elif echo "$INPUT" | jq -e '.tool // .input.command' &> /dev/null; then
128
+ # Legacy Copilot-style: { "tool": "runTerminalCommand", "input": { "command": "..." } }
129
+ GUARDRAIL_TOOL="bash"
130
+ CMD="$(echo "$INPUT" | jq -r '.input.command // .input.cmd // .args[0] // ""' 2> /dev/null)"
131
+ CONTEXT_JSON="$(jq -n -c --arg cmd "$CMD" '{command: $cmd}')"
132
+
50
133
  else
51
- CONTEXT_JSON="$INPUT"
134
+ # Unrecognized input shape: fail-closed
135
+ deny "🛡️ APort: unrecognized hook input — fail-closed policy"
136
+ fi
137
+
138
+ # Use a per-invocation decision file to avoid race conditions with concurrent tool calls
139
+ HOOK_DECISION_FILE="${OPENCLAW_DECISION_FILE:-}"
140
+ if [ -n "$HOOK_DECISION_FILE" ]; then
141
+ HOOK_DECISION_FILE="${HOOK_DECISION_FILE%.json}-$$.json"
142
+ export OPENCLAW_DECISION_FILE="$HOOK_DECISION_FILE"
52
143
  fi
53
144
 
54
- # Call existing bash guardrail: exit 0 = allow, exit 1 = deny (forward config for subprocess)
145
+ # Call core evaluator
55
146
  set +e
56
- OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}" OPENCLAW_PASSPORT_FILE="${OPENCLAW_PASSPORT_FILE:-}" OPENCLAW_DECISION_FILE="${OPENCLAW_DECISION_FILE:-}" "$GUARDRAIL" "$TOOL_NAME" "$CONTEXT_JSON" 2> /dev/null
147
+ "$GUARDRAIL" "$GUARDRAIL_TOOL" "$CONTEXT_JSON" 2> /dev/null
57
148
  GUARDRAIL_EXIT=$?
58
149
  set -e
59
150
 
151
+ # Clean up per-invocation decision file
152
+ cleanup_decision() { [ -n "$HOOK_DECISION_FILE" ] && rm -f "$HOOK_DECISION_FILE" 2> /dev/null; }
153
+
60
154
  if [ "$GUARDRAIL_EXIT" -eq 0 ]; then
155
+ cleanup_decision
61
156
  echo '{"permission":"allow","allowed":true}'
62
157
  exit 0
63
158
  fi
64
159
 
65
- # Deny: output reason from decision file if available (guardrail writes decision before exit 1)
160
+ # Deny: read reason from decision file
66
161
  REASON="Policy denied this action."
67
- if [ -n "${OPENCLAW_DECISION_FILE:-}" ] && [ -f "$OPENCLAW_DECISION_FILE" ] && command -v jq &> /dev/null; then
68
- R=$(jq -r '.reasons[0].message // empty' "$OPENCLAW_DECISION_FILE" 2> /dev/null)
69
- if [ -n "$R" ]; then
70
- REASON="$R"
71
- fi
162
+ if [ -n "$HOOK_DECISION_FILE" ] && [ -f "$HOOK_DECISION_FILE" ]; then
163
+ R="$(jq -r '.reasons[0].message // empty' "$HOOK_DECISION_FILE" 2> /dev/null)"
164
+ [ -n "$R" ] && REASON="$R"
72
165
  fi
73
- # If no decision file was set, try common config dirs so we can show actual deny reason
74
- if [ "$REASON" = "Policy denied this action." ] && command -v jq &> /dev/null; then
166
+ # Fallback: try common config dirs
167
+ if [ "$REASON" = "Policy denied this action." ]; then
75
168
  for DEC in "${OPENCLAW_CONFIG_DIR:-$HOME/.cursor}/aport/decision.json" "$HOME/.cursor/aport/decision.json" "$HOME/.openclaw/aport/decision.json"; do
76
169
  if [ -f "$DEC" ]; then
77
- R=$(jq -r '.reasons[0].message // empty' "$DEC" 2> /dev/null)
78
- if [ -n "$R" ]; then
79
- REASON="$R"
80
- break
81
- fi
170
+ R="$(jq -r '.reasons[0].message // empty' "$DEC" 2> /dev/null)"
171
+ [ -n "$R" ] && REASON="$R" && break
82
172
  fi
83
173
  done
84
174
  fi
85
- # Fallback: help user debug guardrail/script errors
86
- if [ "$REASON" = "Policy denied this action." ]; then
87
- REASON="Policy denied or guardrail error. Check passport and guardrail script (see docs/frameworks/cursor.md)."
88
- fi
89
- echo "{\"permission\":\"deny\",\"allowed\":false,\"agentMessage\":$(echo "$REASON" | jq -Rs .),\"reason\":$(echo "$REASON" | jq -Rs .)}"
90
- exit 2
175
+ cleanup_decision
176
+ deny "🛡️ APort: $REASON"
@@ -45,12 +45,13 @@ run_setup() {
45
45
  if [ -f "$CURSOR_HOOKS_FILE" ] && command -v jq &> /dev/null; then
46
46
  EXISTING=$(cat "$CURSOR_HOOKS_FILE")
47
47
  if echo "$EXISTING" | jq -e '.hooks' &> /dev/null; then
48
- # Add APort hook to beforeShellExecution and preToolUse (avoid duplicate)
48
+ # Add APort hook to all supported lifecycle events (avoid duplicate)
49
49
  NEW_HOOKS=$(echo "$EXISTING" | jq -c --arg cmd "$HOOK_SCRIPT" '
50
- (.hooks.beforeShellExecution // []) as $b |
51
- (.hooks.preToolUse // []) as $p |
52
- .hooks.beforeShellExecution = ($b | map(select(.command != $cmd)) | . + [{ "command": $cmd }]) |
53
- .hooks.preToolUse = ($p | map(select(.command != $cmd)) | . + [{ "command": $cmd }])
50
+ def upsert_hook: map(select(.command != $cmd)) | . + [{ "command": $cmd }];
51
+ .hooks.beforeShellExecution = ((.hooks.beforeShellExecution // []) | upsert_hook) |
52
+ .hooks.preToolUse = ((.hooks.preToolUse // []) | upsert_hook) |
53
+ .hooks.beforeMCPExecution = ((.hooks.beforeMCPExecution // []) | upsert_hook) |
54
+ .hooks.subagentStart = ((.hooks.subagentStart // []) | upsert_hook)
54
55
  ')
55
56
  [ -f "$CURSOR_HOOKS_FILE" ] && cp "$CURSOR_HOOKS_FILE" "${CURSOR_HOOKS_FILE}.bak"
56
57
  echo "$NEW_HOOKS" > "$CURSOR_HOOKS_FILE"
@@ -82,7 +83,9 @@ _write_cursor_hooks_file() {
82
83
  version: 1,
83
84
  hooks: {
84
85
  beforeShellExecution: [{ command: $cmd }],
85
- preToolUse: [{ command: $cmd }]
86
+ preToolUse: [{ command: $cmd }],
87
+ beforeMCPExecution: [{ command: $cmd }],
88
+ subagentStart: [{ command: $cmd }]
86
89
  }
87
90
  }' > "$file"
88
91
  else
@@ -93,7 +96,9 @@ _write_cursor_hooks_file() {
93
96
  "version": 1,
94
97
  "hooks": {
95
98
  "beforeShellExecution": [{"command": "${escaped_cmd}"}],
96
- "preToolUse": [{"command": "${escaped_cmd}"}]
99
+ "preToolUse": [{"command": "${escaped_cmd}"}],
100
+ "beforeMCPExecution": [{"command": "${escaped_cmd}"}],
101
+ "subagentStart": [{"command": "${escaped_cmd}"}]
97
102
  }
98
103
  }
99
104
  EOF
package/docs/RELEASE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Release process and version policy
2
2
 
3
- **Current release:** 1.0.13 (see [CHANGELOG.md](../CHANGELOG.md)).
3
+ **Current release:** 1.0.14 (see [CHANGELOG.md](../CHANGELOG.md)).
4
4
 
5
5
  We keep **one version number** across all published packages (Node core, Python core, and every framework adapter). That avoids “core is 1.2 but CLI is 0.9” and keeps the story simple for users and support.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aporthq/aport-agent-guardrails",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Policy enforcement guardrails for OpenClaw-compatible agent frameworks",
5
5
  "workspaces": [
6
6
  "packages/*",