@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.
- package/bin/aport-create-passport.sh +127 -13
- package/bin/aport-cursor-hook.sh +131 -45
- package/bin/frameworks/cursor.sh +12 -7
- package/docs/RELEASE.md +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
123
|
-
file_write_cap
|
|
124
|
-
web_fetch_cap
|
|
125
|
-
web_browser_cap
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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"
|
package/bin/aport-cursor-hook.sh
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# APort Cursor
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# Output: JSON with "permission": "allow"|"deny"
|
|
6
|
-
# Exit: 0 = allow, 2 = block (deny). Other exits = hook error (
|
|
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
|
|
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
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
145
|
+
# Call core evaluator
|
|
55
146
|
set +e
|
|
56
|
-
|
|
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:
|
|
160
|
+
# Deny: read reason from decision file
|
|
66
161
|
REASON="Policy denied this action."
|
|
67
|
-
if [ -n "$
|
|
68
|
-
R
|
|
69
|
-
|
|
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
|
-
#
|
|
74
|
-
if [ "$REASON" = "Policy denied this action." ]
|
|
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
|
|
78
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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"
|
package/bin/frameworks/cursor.sh
CHANGED
|
@@ -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
|
|
48
|
+
# Add APort hook to all supported lifecycle events (avoid duplicate)
|
|
49
49
|
NEW_HOOKS=$(echo "$EXISTING" | jq -c --arg cmd "$HOOK_SCRIPT" '
|
|
50
|
-
(.
|
|
51
|
-
(.hooks.
|
|
52
|
-
.hooks.
|
|
53
|
-
.hooks.
|
|
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.
|
|
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
|
|