@aporthq/aport-agent-guardrails 1.0.12 → 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/README.md +2 -1
- package/bin/agent-guardrails +13 -6
- package/bin/aport-claude-code-hook.sh +147 -0
- package/bin/aport-create-passport.sh +132 -13
- package/bin/aport-cursor-hook.sh +131 -45
- package/bin/aport-guardrail-bash.sh +7 -3
- package/bin/aport-resolve-paths.sh +17 -1
- package/bin/aport-status.sh +6 -0
- package/bin/frameworks/claude-code.sh +114 -0
- package/bin/frameworks/crewai.sh +3 -0
- package/bin/frameworks/cursor.sh +22 -9
- package/bin/frameworks/langchain.sh +3 -0
- package/bin/frameworks/n8n.sh +3 -0
- package/bin/lib/allowlist.sh +6 -4
- package/bin/lib/config.sh +1 -0
- package/bin/lib/detect.sh +6 -1
- package/bin/lib/validation.sh +27 -7
- package/bin/openclaw +10 -4
- package/docs/RELEASE.md +1 -1
- package/docs/frameworks/claude-code.md +109 -0
- package/docs/frameworks/cursor.md +16 -7
- package/docs/launch/PRD-claude-code-guardrail.md +753 -0
- package/extensions/openclaw-aport/index.ts +13 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -86,12 +86,13 @@ The **kill switch suspends the agent**: once active, every tool call is denied u
|
|
|
86
86
|
|
|
87
87
|
**Two ways to use APort:** (1) **Guardrails (CLI/setup)** — run the installer to create your passport and config; (2) **Core (library)** — use the evaluator or framework callback in your app so each tool call is verified. Each framework doc ([LangChain](docs/frameworks/langchain.md), [CrewAI](docs/frameworks/crewai.md), [Cursor](docs/frameworks/cursor.md), [OpenClaw](docs/frameworks/openclaw.md)) describes both and how to use them for that framework (Python and Node where applicable).
|
|
88
88
|
|
|
89
|
-
**Production-ready today:** OpenClaw (plugin + full installer), Cursor (hooks), **Python** LangChain/CrewAI (`pip install aport-agent-guardrails-langchain` / `aport-agent-guardrails-crewai`), and **Node** (CLI + `@aporthq/aport-agent-guardrails-core`, `-langchain`, `-crewai`, `-cursor` published via CI). See [Deployment readiness](docs/DEPLOYMENT_READINESS.md).
|
|
89
|
+
**Production-ready today:** OpenClaw (plugin + full installer), Cursor (hooks), **Claude Code** (PreToolUse hook), **Python** LangChain/CrewAI (`pip install aport-agent-guardrails-langchain` / `aport-agent-guardrails-crewai`), and **Node** (CLI + `@aporthq/aport-agent-guardrails-core`, `-langchain`, `-crewai`, `-cursor`, `-claude-code` published via CI). See [Deployment readiness](docs/DEPLOYMENT_READINESS.md).
|
|
90
90
|
|
|
91
91
|
| Framework | Doc | Integration | Install |
|
|
92
92
|
|-----------|-----|--------------|--------|
|
|
93
93
|
| **OpenClaw** | [docs/frameworks/openclaw.md](docs/frameworks/openclaw.md) | `before_tool_call` plugin | `npx @aporthq/aport-agent-guardrails openclaw` |
|
|
94
94
|
| **Cursor** | [docs/frameworks/cursor.md](docs/frameworks/cursor.md) | `beforeShellExecution` / `preToolUse` hooks → writes `~/.cursor/hooks.json`. **Runtime enforcement is the bash hook;** the Node package `@aporthq/aport-agent-guardrails-cursor` is a helper only (Evaluator, `getHookPath()`). | `npx @aporthq/aport-agent-guardrails cursor` |
|
|
95
|
+
| **Claude Code** | [docs/frameworks/claude-code.md](docs/frameworks/claude-code.md) | PreToolUse hook → writes `~/.claude/settings.json` (Claude Code format; not Cursor). | `npx @aporthq/aport-agent-guardrails claude-code` |
|
|
95
96
|
| **LangChain / LangGraph** | [docs/frameworks/langchain.md](docs/frameworks/langchain.md) | **Python:** `APortCallback` (`on_tool_start`) | `npx @aporthq/aport-agent-guardrails langchain` then `pip install aport-agent-guardrails-langchain` + `aport-langchain setup` |
|
|
96
97
|
| **CrewAI** | [docs/frameworks/crewai.md](docs/frameworks/crewai.md) | **Python:** `@before_tool_call` hook, `register_aport_guardrail` | `npx @aporthq/aport-agent-guardrails crewai` then `pip install aport-agent-guardrails-crewai` + `aport-crewai setup` |
|
|
97
98
|
| **n8n** | [docs/frameworks/n8n.md](docs/frameworks/n8n.md) | *Coming soon* — custom node and runtime in progress | — |
|
package/bin/agent-guardrails
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# agent-guardrails openclaw ap_xxx # openclaw with agent_id (pass-through)
|
|
7
7
|
# agent-guardrails --framework=langchain
|
|
8
8
|
# agent-guardrails -f crewai
|
|
9
|
-
# Supported: openclaw | langchain | crewai | cursor (n8n coming soon)
|
|
9
|
+
# Supported: openclaw | langchain | crewai | cursor | claude-code (n8n coming soon)
|
|
10
10
|
#
|
|
11
11
|
# Backward compatibility: All arguments after the framework are passed through to
|
|
12
12
|
# the framework script. For OpenClaw, bin/openclaw receives them (e.g. agent_id).
|
|
@@ -72,14 +72,14 @@ if [[ -z "$framework" ]]; then
|
|
|
72
72
|
noninteractive="${APORT_NONINTERACTIVE:-${CI:-}}"
|
|
73
73
|
if [[ -n "$noninteractive" ]]; then
|
|
74
74
|
echo "[aport] ERROR: Multiple frameworks detected: $detected_list" >&2
|
|
75
|
-
echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor" >&2
|
|
75
|
+
echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor | claude-code" >&2
|
|
76
76
|
exit 1
|
|
77
77
|
fi
|
|
78
78
|
echo ""
|
|
79
79
|
echo " APort Agent Guardrails — Framework setup"
|
|
80
80
|
echo " ─────────────────────────────────────────"
|
|
81
81
|
echo " Multiple frameworks detected: $detected_list"
|
|
82
|
-
echo " Choose one: openclaw | langchain | crewai | cursor"
|
|
82
|
+
echo " Choose one: openclaw | langchain | crewai | cursor | claude-code"
|
|
83
83
|
echo " Example: npx @aporthq/agent-guardrails --framework=openclaw"
|
|
84
84
|
echo ""
|
|
85
85
|
read -p " Framework [${framework:-openclaw}]: " choice
|
|
@@ -95,14 +95,14 @@ if [[ -z "$framework" ]]; then
|
|
|
95
95
|
noninteractive="${APORT_NONINTERACTIVE:-${CI:-}}"
|
|
96
96
|
if [[ -n "$noninteractive" ]]; then
|
|
97
97
|
echo "[aport] ERROR: No framework detected in current directory." >&2
|
|
98
|
-
echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor" >&2
|
|
98
|
+
echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor | claude-code" >&2
|
|
99
99
|
exit 1
|
|
100
100
|
fi
|
|
101
101
|
echo ""
|
|
102
102
|
echo " APort Agent Guardrails — Framework setup"
|
|
103
103
|
echo " ─────────────────────────────────────────"
|
|
104
104
|
echo " No framework detected in current directory."
|
|
105
|
-
echo " Choose: openclaw | langchain | crewai | cursor"
|
|
105
|
+
echo " Choose: openclaw | langchain | crewai | cursor | claude-code"
|
|
106
106
|
echo " Example: npx @aporthq/agent-guardrails openclaw"
|
|
107
107
|
echo " npx @aporthq/agent-guardrails --framework=langchain"
|
|
108
108
|
echo ""
|
|
@@ -111,6 +111,13 @@ if [[ -z "$framework" ]]; then
|
|
|
111
111
|
fi
|
|
112
112
|
|
|
113
113
|
framework="$(echo "$framework" | tr '[:upper:]' '[:lower:]')"
|
|
114
|
+
|
|
115
|
+
# SECURITY: Validate framework name (alphanumeric + hyphen only, prevents path traversal)
|
|
116
|
+
if [[ ! "$framework" =~ ^[a-z0-9-]+$ ]]; then
|
|
117
|
+
echo "[aport] ERROR: Invalid framework name: $framework (alphanumeric and hyphens only)" >&2
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
114
121
|
script="$FRAMEWORKS_DIR/${framework}.sh"
|
|
115
122
|
|
|
116
123
|
# n8n: custom node not yet available; warn before running wizard-only script
|
|
@@ -129,5 +136,5 @@ if [[ "$framework" == "openclaw" ]]; then
|
|
|
129
136
|
fi
|
|
130
137
|
|
|
131
138
|
echo "[aport] ERROR: Unknown or unsupported framework: $framework" >&2
|
|
132
|
-
echo " Supported: openclaw, langchain, crewai, cursor (n8n coming soon)" >&2
|
|
139
|
+
echo " Supported: openclaw, langchain, crewai, cursor, claude-code (n8n coming soon)" >&2
|
|
133
140
|
exit 1
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# APort Claude Code hook: reads tool_name + tool_input from JSON stdin,
|
|
3
|
+
# maps to APort policy, calls guardrail, outputs hookSpecificOutput deny or exit 0.
|
|
4
|
+
# Exit 0 = allow, exit 2 = block. Other exits = hook error (Claude Code may fail-open).
|
|
5
|
+
# Output format: Claude Code official schema (hookSpecificOutput.permissionDecision), NOT Cursor format.
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
11
|
+
GUARDRAIL="$ROOT_DIR/bin/aport-guardrail-bash.sh"
|
|
12
|
+
|
|
13
|
+
# Path resolver: probes ~/.claude, ~/.cursor, ~/.openclaw, etc.
|
|
14
|
+
# shellcheck source=bin/aport-resolve-paths.sh
|
|
15
|
+
. "$ROOT_DIR/bin/aport-resolve-paths.sh"
|
|
16
|
+
|
|
17
|
+
# Read stdin
|
|
18
|
+
INPUT=""
|
|
19
|
+
if [ -t 0 ]; then
|
|
20
|
+
INPUT='{}'
|
|
21
|
+
else
|
|
22
|
+
INPUT="$(cat)"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# No input = allow (fail-open for bad input)
|
|
26
|
+
[ -z "$INPUT" ] && exit 0
|
|
27
|
+
|
|
28
|
+
# Parse tool_name and tool_input (requires jq)
|
|
29
|
+
if ! command -v jq &> /dev/null; then
|
|
30
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"🛡️ APort: jq is required"}}'
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Parse with error handling: jq failure must exit 2 (deny), never undefined exit codes
|
|
35
|
+
set +e
|
|
36
|
+
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // "unknown"' 2> /dev/null)"
|
|
37
|
+
JQ_EXIT=$?
|
|
38
|
+
set -e
|
|
39
|
+
if [ "$JQ_EXIT" -ne 0 ] || [ -z "$TOOL_NAME" ]; then
|
|
40
|
+
TOOL_NAME="unknown"
|
|
41
|
+
fi
|
|
42
|
+
set +e
|
|
43
|
+
TOOL_INPUT="$(echo "$INPUT" | jq -c '.tool_input // {}' 2> /dev/null)"
|
|
44
|
+
JQ_EXIT=$?
|
|
45
|
+
set -e
|
|
46
|
+
if [ "$JQ_EXIT" -ne 0 ] || [ -z "$TOOL_INPUT" ]; then
|
|
47
|
+
TOOL_INPUT='{}'
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Safe jq extraction: returns '{}' on any jq error instead of crashing under set -e
|
|
51
|
+
safe_jq() {
|
|
52
|
+
local input="$1" filter="$2"
|
|
53
|
+
local result
|
|
54
|
+
result="$(echo "$input" | jq -c "$filter" 2> /dev/null)" || result='{}'
|
|
55
|
+
[ -z "$result" ] && result='{}'
|
|
56
|
+
echo "$result"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Deny helper: outputs Claude Code hookSpecificOutput JSON and exits 2
|
|
60
|
+
deny() {
|
|
61
|
+
local reason="$1"
|
|
62
|
+
jq -n --arg reason "$reason" \
|
|
63
|
+
--arg event "PreToolUse" \
|
|
64
|
+
'{hookSpecificOutput:{hookEventName:$event,permissionDecision:"deny",permissionDecisionReason:$reason}}'
|
|
65
|
+
exit 2
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Tool name passed to guardrail (must match aport-guardrail-bash.sh case patterns)
|
|
69
|
+
GUARDRAIL_TOOL=""
|
|
70
|
+
CONTEXT_JSON="{}"
|
|
71
|
+
|
|
72
|
+
case "$TOOL_NAME" in
|
|
73
|
+
Bash)
|
|
74
|
+
GUARDRAIL_TOOL="bash"
|
|
75
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{command: (.command // "")}')"
|
|
76
|
+
;;
|
|
77
|
+
Read | Glob | LS | Grep | TodoRead | ToolSearch | AskUserQuestion)
|
|
78
|
+
# Read-family + user-interaction tools: allow without calling evaluator
|
|
79
|
+
exit 0
|
|
80
|
+
;;
|
|
81
|
+
TaskGet | TaskList | TaskOutput | CronList)
|
|
82
|
+
# Read-only task/cron queries: allow without evaluator
|
|
83
|
+
exit 0
|
|
84
|
+
;;
|
|
85
|
+
EnterPlanMode | ExitPlanMode)
|
|
86
|
+
# Internal state transitions: allow without evaluator
|
|
87
|
+
exit 0
|
|
88
|
+
;;
|
|
89
|
+
Write | Edit | MultiEdit | NotebookEdit | TodoWrite)
|
|
90
|
+
GUARDRAIL_TOOL="write"
|
|
91
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{file_path: (.file_path // .path // "")}')"
|
|
92
|
+
;;
|
|
93
|
+
WebSearch | WebFetch)
|
|
94
|
+
GUARDRAIL_TOOL="webfetch"
|
|
95
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // .query // "")}')"
|
|
96
|
+
;;
|
|
97
|
+
Browser)
|
|
98
|
+
GUARDRAIL_TOOL="browser"
|
|
99
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // "")}')"
|
|
100
|
+
;;
|
|
101
|
+
Task | TaskCreate | TaskUpdate | TaskStop | Agent | Skill | EnterWorktree)
|
|
102
|
+
GUARDRAIL_TOOL="session.create"
|
|
103
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .prompt // "")}')"
|
|
104
|
+
;;
|
|
105
|
+
CronCreate | CronDelete)
|
|
106
|
+
GUARDRAIL_TOOL="cron"
|
|
107
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .schedule // "")}')"
|
|
108
|
+
;;
|
|
109
|
+
mcp__*)
|
|
110
|
+
GUARDRAIL_TOOL="mcp.tool"
|
|
111
|
+
CONTEXT_JSON="$TOOL_INPUT"
|
|
112
|
+
;;
|
|
113
|
+
unknown | *)
|
|
114
|
+
# Unknown tool: fail-closed (deny)
|
|
115
|
+
deny "🛡️ APort: unknown tool '$TOOL_NAME' — fail-closed policy"
|
|
116
|
+
;;
|
|
117
|
+
esac
|
|
118
|
+
|
|
119
|
+
# Use a per-invocation decision file to avoid race conditions with concurrent tool calls
|
|
120
|
+
HOOK_DECISION_FILE="${OPENCLAW_DECISION_FILE:-}"
|
|
121
|
+
if [ -n "$HOOK_DECISION_FILE" ]; then
|
|
122
|
+
HOOK_DECISION_FILE="${HOOK_DECISION_FILE%.json}-$$.json"
|
|
123
|
+
export OPENCLAW_DECISION_FILE="$HOOK_DECISION_FILE"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Call core evaluator (guardrail expects tool name, not policy ID)
|
|
127
|
+
set +e
|
|
128
|
+
"$GUARDRAIL" "$GUARDRAIL_TOOL" "$CONTEXT_JSON" 2> /dev/null
|
|
129
|
+
GUARDRAIL_EXIT=$?
|
|
130
|
+
set -e
|
|
131
|
+
|
|
132
|
+
# Clean up per-invocation decision file on exit
|
|
133
|
+
cleanup_decision() { [ -n "$HOOK_DECISION_FILE" ] && rm -f "$HOOK_DECISION_FILE" 2> /dev/null; }
|
|
134
|
+
|
|
135
|
+
if [ "$GUARDRAIL_EXIT" -eq 0 ]; then
|
|
136
|
+
cleanup_decision
|
|
137
|
+
exit 0
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Deny: read reason from decision file
|
|
141
|
+
REASON="Policy denied this action."
|
|
142
|
+
if [ -n "$HOOK_DECISION_FILE" ] && [ -f "$HOOK_DECISION_FILE" ] && command -v jq &> /dev/null; then
|
|
143
|
+
R="$(jq -r '.reasons[0].message // empty' "$HOOK_DECISION_FILE" 2> /dev/null)"
|
|
144
|
+
[ -n "$R" ] && REASON="$R"
|
|
145
|
+
fi
|
|
146
|
+
cleanup_decision
|
|
147
|
+
deny "🛡️ APort: $REASON"
|
|
@@ -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
|
|
|
@@ -503,6 +611,11 @@ else
|
|
|
503
611
|
EOF
|
|
504
612
|
fi
|
|
505
613
|
|
|
614
|
+
# Archive existing passport before overwrite
|
|
615
|
+
if [ -f "$PASSPORT_FILE" ]; then
|
|
616
|
+
cp "$PASSPORT_FILE" "${PASSPORT_FILE}.bak"
|
|
617
|
+
fi
|
|
618
|
+
|
|
506
619
|
# Format JSON with jq if available
|
|
507
620
|
if command -v jq &> /dev/null; then
|
|
508
621
|
jq . "$PASSPORT_FILE.tmp" > "$PASSPORT_FILE"
|
|
@@ -535,7 +648,13 @@ echo " 🔐 Capabilities:"
|
|
|
535
648
|
[ "$pr_cap" = "y" ] || [ "$pr_cap" = "Y" ] && echo " • Create and merge pull requests"
|
|
536
649
|
[ "$exec_cap" = "y" ] || [ "$exec_cap" = "Y" ] && echo " • Execute system commands"
|
|
537
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"
|
|
538
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"
|
|
539
658
|
echo ""
|
|
540
659
|
echo " 📝 Next steps:"
|
|
541
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"
|