@aporthq/aport-agent-guardrails 1.0.13 → 1.0.15
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/agent-guardrails +19 -1
- 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
- package/skills/aport-agent-guardrail/SKILL.md +107 -330
package/bin/agent-guardrails
CHANGED
|
@@ -54,7 +54,25 @@ while [[ $# -gt 0 ]]; do
|
|
|
54
54
|
esac
|
|
55
55
|
done
|
|
56
56
|
|
|
57
|
-
# If no framework from args,
|
|
57
|
+
# If no framework from args, check if first REST argument is a framework name
|
|
58
|
+
if [[ -z "$framework" ]] && [[ ${#REST[@]} -gt 0 ]]; then
|
|
59
|
+
first_arg="${REST[0]}"
|
|
60
|
+
# Check if first arg looks like a framework name (lowercase alphanumeric + hyphen)
|
|
61
|
+
if [[ "$first_arg" =~ ^[a-z0-9-]+$ ]]; then
|
|
62
|
+
# Valid framework names
|
|
63
|
+
valid_frameworks=(openclaw langchain crewai cursor claude-code n8n)
|
|
64
|
+
for valid_fw in "${valid_frameworks[@]}"; do
|
|
65
|
+
if [[ "$first_arg" == "$valid_fw" ]]; then
|
|
66
|
+
framework="$first_arg"
|
|
67
|
+
# Remove framework from REST so remaining args are pass-through
|
|
68
|
+
REST=("${REST[@]:1}")
|
|
69
|
+
break
|
|
70
|
+
fi
|
|
71
|
+
done
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# If still no framework from args, try APORT_FRAMEWORK (non-interactive) or detection
|
|
58
76
|
if [[ -z "$framework" ]]; then
|
|
59
77
|
if [[ -n "${APORT_FRAMEWORK:-}" ]]; then
|
|
60
78
|
framework="$APORT_FRAMEWORK"
|
|
@@ -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
|
|
package/package.json
CHANGED
|
@@ -1,91 +1,84 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aport-agent-guardrail
|
|
3
|
-
description:
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
description: >
|
|
4
|
+
Pre-action authorization for AI agents. Installs an OpenClaw before_tool_call hook that
|
|
5
|
+
evaluates every tool call against a passport and policy before execution. Blocks unauthorized
|
|
6
|
+
commands, data exfiltration, and policy violations. Supports local (offline) and hosted
|
|
7
|
+
(API) passport modes. Requires Node.js 18+ and npx.
|
|
8
|
+
metadata:
|
|
9
|
+
author: uchibeke
|
|
10
|
+
version: 1.1.11
|
|
11
|
+
tags: security, guardrails, authorization, ai-agent, openclaw, aport, policy-enforcement
|
|
6
12
|
---
|
|
7
13
|
|
|
8
14
|
# APort Agent Guardrail
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
Pre-action authorization for AI agents. Installs an OpenClaw `before_tool_call` hook that
|
|
17
|
+
evaluates every tool call against a passport (identity + capabilities + limits) and policy
|
|
18
|
+
**before** it executes. If the policy denies the call, the tool does not run.
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
**Pre-action authorization for AI agents.** Every tool call is evaluated against a passport (identity + capabilities + limits) and policy **before** it executes. If denied, the tool never runs.
|
|
20
|
+
This skill provides setup instructions. The enforcement logic comes from the
|
|
21
|
+
[@aporthq/aport-agent-guardrails](https://github.com/aporthq/aport-agent-guardrails)
|
|
22
|
+
npm package, which is open-source (Apache 2.0) and can be audited before installation.
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
- ✅ **Deterministic enforcement** – Runs in `before_tool_call` hook; agent cannot bypass
|
|
20
|
-
- ✅ **Blocks malicious actions** – Unauthorized commands, data exfiltration, API abuse prevented
|
|
21
|
-
- ✅ **Structured policies** – Based on [Open Agent Passport (OAP) v1.0](https://github.com/aporthq/aport-spec/tree/main)
|
|
22
|
-
- ✅ **Fail-closed by default** – Errors deny tool execution (security over availability)
|
|
23
|
-
- ✅ **Audit trail** – Every decision logged with tamper-evident hashes
|
|
24
|
-
- ✅ **Framework-agnostic** – OpenClaw, IronClaw, PicoClaw, and compatible runtimes
|
|
24
|
+
## When to use this skill
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
- Data exfiltration → File access, messaging, web requests controlled
|
|
31
|
-
- Resource exhaustion → Rate limits, size caps enforced
|
|
32
|
-
|
|
33
|
-
**Install once, protected forever.** The plugin runs automatically on every tool call.
|
|
34
|
-
|
|
35
|
-
---
|
|
26
|
+
- User wants to add guardrails to their AI agent setup
|
|
27
|
+
- User asks about protecting against unauthorized tool calls
|
|
28
|
+
- User wants pre-action authorization for OpenClaw, IronClaw, or PicoClaw agents
|
|
29
|
+
- User needs audit trails for AI agent actions
|
|
36
30
|
|
|
37
|
-
##
|
|
31
|
+
## How it works
|
|
38
32
|
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
```
|
|
34
|
+
User Request -> Agent Decision -> APort Hook -> [ALLOW/DENY] -> Tool Execution
|
|
35
|
+
|
|
|
36
|
+
Policy + Passport
|
|
37
|
+
```
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
1. Agent decides to use a tool (e.g., run a shell command)
|
|
40
|
+
2. OpenClaw fires the `before_tool_call` hook
|
|
41
|
+
3. APort loads the passport, maps the tool to a policy, checks allowlists and limits
|
|
42
|
+
4. Decision: ALLOW (tool runs) or DENY (tool blocked)
|
|
43
|
+
5. Decision is logged to the audit trail
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
Enforcement runs in the OpenClaw hook layer, not in agent prompts. However, like any
|
|
46
|
+
application-layer security control, it depends on the integrity of the runtime environment
|
|
47
|
+
(OS, OpenClaw, filesystem). See the [Security Model](https://github.com/aporthq/aport-agent-guardrails/blob/main/docs/SECURITY_MODEL.md) for trust boundaries.
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
# Get agent_id from aport.io
|
|
53
|
-
npx @aporthq/aport-agent-guardrails <agent_id>
|
|
54
|
-
```
|
|
49
|
+
## Prerequisites
|
|
55
50
|
|
|
56
|
-
|
|
51
|
+
Check these before starting:
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
1. **Node.js 18+** and **npx** — run `node -v` to verify (must show v18 or higher)
|
|
54
|
+
2. **OpenClaw** (or compatible runtime) — the hook registers as an OpenClaw plugin
|
|
59
55
|
|
|
60
|
-
##
|
|
56
|
+
## Installation
|
|
61
57
|
|
|
62
|
-
###
|
|
58
|
+
### Quick start (recommended)
|
|
63
59
|
|
|
64
60
|
```bash
|
|
65
61
|
npx @aporthq/aport-agent-guardrails
|
|
66
62
|
```
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
1. Create or load passport (local file or hosted from aport.io)
|
|
64
|
+
The wizard will:
|
|
65
|
+
1. Create or load a passport (local file or hosted from aport.io)
|
|
70
66
|
2. Configure capabilities and limits
|
|
71
|
-
3.
|
|
72
|
-
4. Set up wrapper scripts
|
|
67
|
+
3. Register the OpenClaw plugin (adds `before_tool_call` hook)
|
|
68
|
+
4. Set up wrapper scripts under `~/.openclaw/`
|
|
73
69
|
|
|
74
|
-
|
|
70
|
+
After install, the hook runs on every tool call automatically.
|
|
75
71
|
|
|
76
|
-
###
|
|
72
|
+
### With hosted passport (optional)
|
|
77
73
|
|
|
78
74
|
```bash
|
|
79
75
|
npx @aporthq/aport-agent-guardrails <agent_id>
|
|
80
76
|
```
|
|
81
77
|
|
|
82
|
-
Get `agent_id` at [aport.io](https://aport.io/builder/create/) for
|
|
83
|
-
|
|
84
|
-
- Global suspend (<200ms across all systems)
|
|
85
|
-
- Centralized audit and compliance dashboards
|
|
86
|
-
- Team collaboration
|
|
78
|
+
Get `agent_id` at [aport.io](https://aport.io/builder/create/) for signed decisions,
|
|
79
|
+
global suspend, and centralized audit dashboards.
|
|
87
80
|
|
|
88
|
-
###
|
|
81
|
+
### From source
|
|
89
82
|
|
|
90
83
|
```bash
|
|
91
84
|
git clone https://github.com/aporthq/aport-agent-guardrails
|
|
@@ -93,319 +86,103 @@ cd aport-agent-guardrails
|
|
|
93
86
|
./bin/openclaw
|
|
94
87
|
```
|
|
95
88
|
|
|
96
|
-
|
|
97
|
-
- [QuickStart: OpenClaw Plugin](https://github.com/aporthq/aport-agent-guardrails/blob/main/docs/QUICKSTART_OPENCLAW_PLUGIN.md)
|
|
98
|
-
- [Hosted passport setup](https://github.com/aporthq/aport-agent-guardrails/blob/main/docs/HOSTED_PASSPORT_SETUP.md)
|
|
89
|
+
### What gets installed
|
|
99
90
|
|
|
100
|
-
|
|
91
|
+
Files created under `~/.openclaw/`:
|
|
92
|
+
- Plugin config in `config.yaml` or `openclaw.json`
|
|
93
|
+
- Wrapper scripts in `.skills/aport-guardrail*.sh`
|
|
94
|
+
- `aport/passport.json` (local mode only)
|
|
95
|
+
- `aport/decision.json` and `aport/audit.log` (created at runtime)
|
|
101
96
|
|
|
102
|
-
|
|
97
|
+
Total disk usage: ~100KB for scripts + passport/decision files.
|
|
103
98
|
|
|
104
|
-
|
|
99
|
+
## Usage
|
|
105
100
|
|
|
106
|
-
|
|
101
|
+
After installation, the hook runs automatically on every tool call:
|
|
107
102
|
|
|
108
103
|
```bash
|
|
109
|
-
#
|
|
104
|
+
# Allowed command — hook approves, tool executes
|
|
110
105
|
agent> run git status
|
|
111
|
-
#
|
|
106
|
+
# APort: passport checked -> policy evaluated -> ALLOW
|
|
112
107
|
|
|
108
|
+
# Blocked command — hook denies, tool does not run
|
|
113
109
|
agent> run rm -rf /
|
|
114
|
-
#
|
|
110
|
+
# APort: passport checked -> blocked pattern detected -> DENY
|
|
115
111
|
```
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
### Testing the guardrail (optional)
|
|
120
|
-
|
|
121
|
-
**Direct script calls for testing or automation:**
|
|
113
|
+
### Testing the hook manually
|
|
122
114
|
|
|
123
115
|
```bash
|
|
124
|
-
# Test allowed command
|
|
116
|
+
# Test allowed command (exit 0 = ALLOW)
|
|
125
117
|
~/.openclaw/.skills/aport-guardrail.sh system.command.execute '{"command":"ls"}'
|
|
126
|
-
# Exit 0 = ALLOW
|
|
127
118
|
|
|
128
|
-
# Test blocked command
|
|
119
|
+
# Test blocked command (exit 1 = DENY)
|
|
129
120
|
~/.openclaw/.skills/aport-guardrail.sh system.command.execute '{"command":"rm -rf /"}'
|
|
130
|
-
# Exit 1 = DENY
|
|
131
|
-
|
|
132
|
-
# Test messaging
|
|
133
|
-
~/.openclaw/.skills/aport-guardrail.sh messaging.message.send '{"channel":"whatsapp","to":"+15551234567"}'
|
|
134
121
|
```
|
|
135
122
|
|
|
136
|
-
|
|
137
|
-
-
|
|
138
|
-
- `1` = DENY (reason in decision.json)
|
|
139
|
-
|
|
140
|
-
**Decision logs:**
|
|
141
|
-
- Latest: `~/.openclaw/aport/decision.json`
|
|
123
|
+
Decision logs:
|
|
124
|
+
- Latest decision: `~/.openclaw/aport/decision.json`
|
|
142
125
|
- Audit trail: `~/.openclaw/aport/audit.log`
|
|
143
|
-
- API mode: Signed receipts via APort API
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## 🔍 How It Works
|
|
148
126
|
|
|
149
|
-
|
|
127
|
+
## Modes
|
|
150
128
|
|
|
151
|
-
|
|
152
|
-
User Request → Agent Decision → APort Check → [ALLOW/DENY] → Tool Execution
|
|
153
|
-
↑
|
|
154
|
-
Policy + Passport
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
1. **User makes request** (e.g., "Deploy to production")
|
|
158
|
-
2. **Agent decides to use tool** (e.g., exec.run with git push)
|
|
159
|
-
3. **OpenClaw fires hook** (`before_tool_call`)
|
|
160
|
-
4. **APort evaluates:**
|
|
161
|
-
- Load passport (identity, capabilities, limits)
|
|
162
|
-
- Map tool → policy (exec → system.command.execute.v1)
|
|
163
|
-
- Check allowlist, blocked patterns, rate limits
|
|
164
|
-
5. **Decision:** ALLOW or DENY
|
|
165
|
-
6. **Audit:** Log decision with timestamp, policy, reason codes
|
|
166
|
-
|
|
167
|
-
**Agent cannot bypass.** Hook is registered by OpenClaw, not controlled by prompts.
|
|
168
|
-
|
|
169
|
-
### What Gets Installed
|
|
170
|
-
|
|
171
|
-
**Plugin registration:**
|
|
172
|
-
- OpenClaw plugin added to config (enforces before_tool_call)
|
|
173
|
-
- TypeScript/JavaScript plugin loaded on OpenClaw start
|
|
174
|
-
|
|
175
|
-
**Files created (under ~/.openclaw/):**
|
|
176
|
-
- `config.yaml` or `openclaw.json` – Plugin configuration
|
|
177
|
-
- `.skills/aport-guardrail*.sh` – Wrapper scripts for local/API evaluation
|
|
178
|
-
- `aport/passport.json` – Your agent passport (local mode only)
|
|
179
|
-
- `aport/decision.json` – Latest decision (runtime)
|
|
180
|
-
- `aport/audit.log` – Audit trail (runtime)
|
|
181
|
-
|
|
182
|
-
**Total disk usage:** ~100KB scripts + your passport/decisions
|
|
183
|
-
|
|
184
|
-
**Review code:** [GitHub repository](https://github.com/aporthq/aport-agent-guardrails)
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## 🌐 Network and Privacy
|
|
129
|
+
### Local mode (default)
|
|
189
130
|
|
|
190
|
-
|
|
131
|
+
- All evaluation happens on your machine, zero network calls
|
|
132
|
+
- Passport stored locally at `~/.openclaw/aport/passport.json`
|
|
133
|
+
- Works offline
|
|
134
|
+
- Note: local passport file must be protected from tampering (standard filesystem permissions)
|
|
191
135
|
|
|
192
|
-
|
|
193
|
-
- ✅ All evaluation on your machine
|
|
194
|
-
- ✅ Passport stored locally
|
|
195
|
-
- ✅ Decisions stay local
|
|
196
|
-
- ✅ Full privacy
|
|
197
|
-
- ✅ Works offline
|
|
136
|
+
### API mode (optional)
|
|
198
137
|
|
|
199
|
-
|
|
138
|
+
- Passport hosted in the aport.io registry (not stored locally)
|
|
139
|
+
- Signed decisions (Ed25519) for tamper-evident audit trails
|
|
140
|
+
- Global suspend across all systems
|
|
141
|
+
- Centralized compliance dashboards
|
|
142
|
+
- Sends tool name + context to API (does not send file contents, env vars, or credentials)
|
|
200
143
|
|
|
201
|
-
|
|
144
|
+
## Environment variables
|
|
202
145
|
|
|
203
|
-
|
|
204
|
-
- Tool name + context → APort API for policy evaluation
|
|
205
|
-
- Hosted passport fetched from registry (if using agent_id)
|
|
206
|
-
- Signed decisions returned (Ed25519 cryptographic signatures)
|
|
146
|
+
All optional. Local mode requires no environment variables.
|
|
207
147
|
|
|
208
|
-
|
|
209
|
-
- ✅ Cryptographically signed decisions
|
|
210
|
-
- ✅ Court-admissible audit trail
|
|
211
|
-
- ✅ Global suspend across all systems
|
|
212
|
-
- ✅ Centralized compliance dashboards
|
|
213
|
-
- ✅ No local passport tampering possible
|
|
214
|
-
|
|
215
|
-
**API endpoint:** `https://api.aport.io` (or custom via APORT_API_URL)
|
|
216
|
-
|
|
217
|
-
**Data sent:**
|
|
218
|
-
- Tool name (e.g., "system.command.execute")
|
|
219
|
-
- Context (e.g., {"command": "ls"})
|
|
220
|
-
- Passport (if local) or agent_id (if hosted)
|
|
221
|
-
|
|
222
|
-
**Data NOT sent:**
|
|
223
|
-
- File contents
|
|
224
|
-
- Environment variables
|
|
225
|
-
- API keys or credentials
|
|
226
|
-
- Unrelated system information
|
|
227
|
-
|
|
228
|
-
**To verify:** Use local mode (no network) or inspect open-source code.
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
## ⚙️ Environment Variables
|
|
233
|
-
|
|
234
|
-
| Variable | When Used | Purpose |
|
|
148
|
+
| Variable | When used | Purpose |
|
|
235
149
|
|----------|-----------|---------|
|
|
236
|
-
| `APORT_API_URL` | API mode | Override endpoint (default: `https://api.aport.io`)
|
|
237
|
-
| `APORT_AGENT_ID` | Hosted passport | Passport ID from aport.io
|
|
238
|
-
| `APORT_API_KEY` | If API requires auth | Authentication token
|
|
239
|
-
|
|
240
|
-
**Local mode:** No environment variables needed. Passport read from `~/.openclaw/aport/passport.json`.
|
|
150
|
+
| `APORT_API_URL` | API mode | Override endpoint (default: `https://api.aport.io`) |
|
|
151
|
+
| `APORT_AGENT_ID` | Hosted passport | Passport ID from aport.io |
|
|
152
|
+
| `APORT_API_KEY` | If API requires auth | Authentication token |
|
|
241
153
|
|
|
242
|
-
|
|
154
|
+
## Default protections
|
|
243
155
|
|
|
244
|
-
|
|
156
|
+
- **Shell commands** — Allowlist enforcement, 40+ blocked patterns (`rm -rf`, `sudo`, `chmod 777`, etc.), interpreter bypass detection
|
|
157
|
+
- **Messaging** — Rate limits, recipient allowlist, channel restrictions
|
|
158
|
+
- **File access** — Path restrictions, blocks access to `.env`, SSH keys, system directories
|
|
159
|
+
- **Web requests** — Domain allowlist, SSRF protection, rate limiting
|
|
160
|
+
- **Git operations** — PR size limits, branch restrictions
|
|
245
161
|
|
|
246
|
-
##
|
|
162
|
+
## Tool name mapping
|
|
247
163
|
|
|
248
|
-
|
|
|
249
|
-
|
|
164
|
+
| Agent action | Tool name | Policy checks |
|
|
165
|
+
|--------------|-----------|---------------|
|
|
250
166
|
| Shell commands | `system.command.execute` | Allowlist, blocked patterns |
|
|
251
|
-
| WhatsApp/Email/Slack | `messaging.message.send` | Rate limits, recipient allowlist |
|
|
252
|
-
|
|
|
167
|
+
| Messaging (WhatsApp/Email/Slack) | `messaging.message.send` | Rate limits, recipient allowlist |
|
|
168
|
+
| PRs | `git.create_pr`, `git.merge` | PR size, branch restrictions |
|
|
253
169
|
| MCP tools | `mcp.tool.execute` | Server/tool allowlist |
|
|
254
|
-
| Data export | `data.export` | Row limits, PII filtering |
|
|
255
170
|
| File read/write | `data.file.read`, `data.file.write` | Path restrictions |
|
|
256
|
-
| Web requests | `web.fetch`, `web.browser` | Domain allowlist
|
|
257
|
-
|
|
258
|
-
**Context format:** Valid JSON, e.g., `'{"command":"ls"}'` or `'{"channel":"whatsapp","to":"+1..."}'`
|
|
171
|
+
| Web requests | `web.fetch`, `web.browser` | Domain allowlist |
|
|
259
172
|
|
|
260
|
-
|
|
173
|
+
## Troubleshooting
|
|
261
174
|
|
|
262
|
-
|
|
175
|
+
| Problem | Fix |
|
|
176
|
+
|---------|-----|
|
|
177
|
+
| Plugin not enforcing | Check `openclaw plugin list` shows aport-guardrail |
|
|
178
|
+
| Connection refused (API mode) | Verify `APORT_API_URL` is reachable |
|
|
179
|
+
| Tool blocked unexpectedly | Check `~/.openclaw/aport/decision.json` for deny reason |
|
|
180
|
+
| npx not found | Install Node.js 18+: https://nodejs.org |
|
|
263
181
|
|
|
264
|
-
|
|
265
|
-
- Allowlist enforcement (only specified commands run)
|
|
266
|
-
- 40+ blocked patterns: `rm -rf`, `sudo`, `chmod 777`, `dd if=`, `mkfs`, etc.
|
|
267
|
-
- Interpreter bypasses blocked: `python -c`, `node -e`, `base64` encoding
|
|
268
|
-
- Command injection patterns detected
|
|
182
|
+
## Documentation
|
|
269
183
|
|
|
270
|
-
|
|
271
|
-
- Rate limits (msgs_per_min, msgs_per_day)
|
|
272
|
-
- Recipient allowlist
|
|
273
|
-
- Channel restrictions
|
|
274
|
-
|
|
275
|
-
**File access (data.file.read/write.v1):**
|
|
276
|
-
- Path restrictions (block /etc, /bin, system directories)
|
|
277
|
-
- Prevent .env, SSH key theft
|
|
278
|
-
|
|
279
|
-
**Web requests (web.fetch/browser.v1):**
|
|
280
|
-
- Domain allowlist
|
|
281
|
-
- SSRF protection (block private IPs)
|
|
282
|
-
- Rate limiting
|
|
283
|
-
|
|
284
|
-
**Git operations (code.repository.merge.v1):**
|
|
285
|
-
- PR size limits
|
|
286
|
-
- Branch restrictions
|
|
287
|
-
- Review requirements
|
|
288
|
-
|
|
289
|
-
**All policies at:** https://aport.io/policy-packs
|
|
290
|
-
|
|
291
|
-
---
|
|
292
|
-
|
|
293
|
-
## 🔐 Security Model
|
|
294
|
-
|
|
295
|
-
### What APort Protects
|
|
296
|
-
|
|
297
|
-
**✅ Agent action security:**
|
|
298
|
-
- Prompt injection (hook-based enforcement, not prompt-based)
|
|
299
|
-
- Malicious third-party skills
|
|
300
|
-
- Unauthorized commands
|
|
301
|
-
- Data exfiltration via files, messaging, web requests
|
|
302
|
-
- Resource exhaustion (rate/size limits)
|
|
303
|
-
|
|
304
|
-
### Trust Model
|
|
305
|
-
|
|
306
|
-
**APort operates at the application layer** (between agent decision and tool execution).
|
|
307
|
-
|
|
308
|
-
**You must trust:**
|
|
309
|
-
- Your operating system (file permissions, process isolation)
|
|
310
|
-
- OpenClaw runtime (hooks execute correctly)
|
|
311
|
-
- APort code (open-source, verifiable)
|
|
312
|
-
|
|
313
|
-
**Local mode additionally trusts:**
|
|
314
|
-
- Filesystem integrity (passport not tampered)
|
|
315
|
-
|
|
316
|
-
**API mode eliminates:**
|
|
317
|
-
- Local passport tampering (fetched from API)
|
|
318
|
-
- Decision tampering (cryptographically signed)
|
|
319
|
-
|
|
320
|
-
**Out of scope (OS/infrastructure security):**
|
|
321
|
-
- File system compromise
|
|
322
|
-
- OpenClaw CVEs
|
|
323
|
-
- Network attacks (MITM, DNS poisoning)
|
|
324
|
-
- Supply chain attacks
|
|
325
|
-
|
|
326
|
-
**This is standard for application-layer authorization** (same model as OAuth, IAM, policy engines).
|
|
327
|
-
|
|
328
|
-
---
|
|
329
|
-
|
|
330
|
-
## 🎯 Use Cases
|
|
331
|
-
|
|
332
|
-
**Protect against malicious skills:**
|
|
333
|
-
- Install APort before adding community skills
|
|
334
|
-
- Every skill's tool calls are checked
|
|
335
|
-
- Malicious actions blocked before execution
|
|
336
|
-
|
|
337
|
-
**Compliance and audit:**
|
|
338
|
-
- Tamper-evident decision logs
|
|
339
|
-
- Court-admissible audit trail (API mode with Ed25519 signatures)
|
|
340
|
-
- SOC 2, HIPAA, SOX compliance support
|
|
341
|
-
|
|
342
|
-
**Team deployments:**
|
|
343
|
-
- Shared passport across systems (global suspend)
|
|
344
|
-
- Centralized policy updates
|
|
345
|
-
- Consistent enforcement
|
|
346
|
-
|
|
347
|
-
**Air-gapped environments:**
|
|
348
|
-
- Use local mode (zero network)
|
|
349
|
-
- All evaluation on-premise
|
|
350
|
-
- Self-hosted policy packs
|
|
351
|
-
|
|
352
|
-
---
|
|
353
|
-
|
|
354
|
-
## 📚 Documentation
|
|
355
|
-
|
|
356
|
-
**APort Guardrails:**
|
|
184
|
+
- [Source code](https://github.com/aporthq/aport-agent-guardrails) (Apache 2.0)
|
|
357
185
|
- [QuickStart: OpenClaw Plugin](https://github.com/aporthq/aport-agent-guardrails/blob/main/docs/QUICKSTART_OPENCLAW_PLUGIN.md)
|
|
358
186
|
- [Security Model & Trust Boundaries](https://github.com/aporthq/aport-agent-guardrails/blob/main/docs/SECURITY_MODEL.md)
|
|
359
187
|
- [Hosted Passport Setup](https://github.com/aporthq/aport-agent-guardrails/blob/main/docs/HOSTED_PASSPORT_SETUP.md)
|
|
360
|
-
- [
|
|
361
|
-
- [Verification Methods (Local vs API)](https://github.com/aporthq/aport-agent-guardrails/blob/main/docs/VERIFICATION_METHODS.md)
|
|
362
|
-
|
|
363
|
-
**OpenClaw:**
|
|
364
|
-
- [CLI: skills](https://docs.openclaw.ai/cli/skills)
|
|
365
|
-
- [Skills Documentation](https://docs.openclaw.ai/tools/skills)
|
|
366
|
-
- [Skills Config](https://docs.openclaw.ai/tools/skills-config)
|
|
367
|
-
- [ClawHub](https://docs.openclaw.ai/tools/clawhub)
|
|
368
|
-
|
|
369
|
-
**Security:**
|
|
370
|
-
- [SECURITY.md](https://github.com/aporthq/aport-agent-guardrails/blob/main/SECURITY.md) - Prompt injection, Cisco findings
|
|
371
|
-
- [OAP Specification](https://github.com/aporthq/aport-spec/tree/main) - Open Agent Passport standard
|
|
372
|
-
|
|
373
|
-
---
|
|
374
|
-
|
|
375
|
-
## 🤝 Support and Community
|
|
376
|
-
|
|
377
|
-
**GitHub:** [aporthq/aport-agent-guardrails](https://github.com/aporthq/aport-agent-guardrails)
|
|
378
|
-
**Website:** [aport.io](https://aport.io)
|
|
379
|
-
**Issues:** [GitHub Issues](https://github.com/aporthq/aport-agent-guardrails/issues)
|
|
380
|
-
|
|
381
|
-
**Open-source:** Apache 2.0 License
|
|
382
|
-
**Code review:** All code publicly available for inspection
|
|
383
|
-
|
|
384
|
-
---
|
|
385
|
-
|
|
386
|
-
## ❓ FAQ
|
|
387
|
-
|
|
388
|
-
**Q: Does this slow down my agent?**
|
|
389
|
-
A: Minimal overhead. API mode: ~60-100ms. Local mode: <300ms. Runs in parallel with agent thinking.
|
|
390
|
-
|
|
391
|
-
**Q: Can I use this offline?**
|
|
392
|
-
A: Yes. Local mode works without network connectivity.
|
|
393
|
-
|
|
394
|
-
**Q: What if I need custom policies?**
|
|
395
|
-
A: API mode: Pass custom policy JSON in request. Local mode: Edit bash script or use API mode.
|
|
396
|
-
|
|
397
|
-
**Q: How do I suspend my agent?**
|
|
398
|
-
A: Local: Set passport status to "suspended". Hosted: Log in to aport.io and suspend (global effect).
|
|
399
|
-
|
|
400
|
-
**Q: Is my data sent to APort?**
|
|
401
|
-
A: Local mode: No. API mode: Tool name + context only (no credentials, file contents, or env vars).
|
|
402
|
-
|
|
403
|
-
**Q: Can the agent bypass this?**
|
|
404
|
-
A: No. Enforcement is in the platform hook (`before_tool_call`), not controllable by prompts.
|
|
405
|
-
|
|
406
|
-
**Q: What happens if APort errors?**
|
|
407
|
-
A: Default: Tool blocked (fail-closed). Configurable via `failClosed` setting.
|
|
408
|
-
|
|
409
|
-
---
|
|
410
|
-
|
|
411
|
-
**Made with 🛡️ by [APort](https://aport.io) · Open-source on [GitHub](https://github.com/aporthq/aport-agent-guardrails) · Apache 2.0 License**
|
|
188
|
+
- [OAP Specification](https://github.com/aporthq/aport-spec/tree/main)
|