@aporthq/aport-agent-guardrails 1.0.26 → 1.0.28
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 +30 -6
- package/bin/aport-claude-code-hook.sh +38 -16
- package/bin/aport-create-passport.sh +3 -2
- package/bin/aport-cursor-hook.sh +49 -18
- package/bin/aport-guardrail-api.sh +2 -0
- package/bin/aport-guardrail-bash.sh +15 -0
- package/bin/aport-resolve-paths.sh +67 -40
- package/bin/frameworks/claude-code.sh +11 -1
- package/bin/frameworks/cursor.sh +6 -1
- package/bin/frameworks/generic.sh +9 -0
- package/bin/lib/framework-hook-paths.sh +96 -0
- package/bin/lib/guardrail-mode.sh +44 -3
- package/bin/lib/hook-read-policy.sh +61 -0
- package/bin/lib/quick-hosted.sh +164 -0
- package/bin/lib/tool-mapping.sh +5 -1
- package/bin/openclaw +76 -45
- package/docs/FRAMEWORK_TOOL_MAPPING_AUDIT.md +158 -0
- package/docs/HOSTED_PASSPORT_SETUP.md +29 -4
- package/docs/QUICKSTART.md +9 -3
- package/docs/README.md +3 -3
- package/docs/RELEASE.md +1 -1
- package/docs/TOOL_POLICY_MAPPING.md +6 -1
- package/docs/VERIFICATION_METHODS.md +1 -1
- package/docs/frameworks/claude-code.md +34 -7
- package/docs/frameworks/cursor.md +4 -4
- package/docs/frameworks/deerflow.md +12 -8
- package/extensions/openclaw-aport/CHANGELOG.md +4 -0
- package/extensions/openclaw-aport/local-evaluator.js +17 -0
- package/extensions/openclaw-aport/openclaw.plugin.json +1 -1
- package/extensions/openclaw-aport/package-lock.json +2 -2
- package/extensions/openclaw-aport/package.json +1 -1
- package/extensions/openclaw-aport/tool-mapping.js +33 -4
- package/package.json +1 -1
- package/python/aport_guardrails/core/tool-pack-mapping.json +111 -1
- package/src/evaluator.js +8 -2
package/README.md
CHANGED
|
@@ -46,17 +46,41 @@ From the live APort Vault adversarial testbed:
|
|
|
46
46
|
|
|
47
47
|
## Start Here
|
|
48
48
|
|
|
49
|
-
### Install in
|
|
49
|
+
### Install in 60 seconds
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
npx @aporthq/aport-agent-guardrails
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
- Choose your framework: `openclaw`, `cursor`, `claude-code`, `langchain`, `crewai`, `deerflow`, `n8n`
|
|
56
|
-
-
|
|
57
|
-
-
|
|
56
|
+
- Claude Code direct: `npx @aporthq/aport-agent-guardrails claude-code`
|
|
57
|
+
- Curl install URL: `curl -fsSL https://aport.io/install.sh | bash -s -- claude-code`
|
|
58
|
+
- Existing hosted passport: `npx @aporthq/aport-agent-guardrails claude-code <agent_id>`
|
|
58
59
|
- Reset a framework to a clean APort state: `npx @aporthq/aport-agent-guardrails reset claude-code --yes`
|
|
59
60
|
|
|
61
|
+
When prompted for passport setup, the choices are:
|
|
62
|
+
|
|
63
|
+
1. `Create hosted APort passport now` — recommended; creates a hosted passport and narrow setup key.
|
|
64
|
+
2. `Use existing hosted passport ID` — paste an existing `agent_id`.
|
|
65
|
+
3. `Create local passport file` — offline/local JSON passport.
|
|
66
|
+
|
|
67
|
+
For a new hosted setup, choose option `1`. The installer creates a passport, creates a narrow setup key, writes the framework hook/config, and starts sending decisions to APort. For non-interactive installs:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx --yes @aporthq/aport-agent-guardrails claude-code \
|
|
71
|
+
--quick-hosted \
|
|
72
|
+
--email you@example.com \
|
|
73
|
+
--non-interactive
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Equivalent environment-variable form:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
APORT_OWNER_EMAIL="you@example.com" \
|
|
80
|
+
APORT_QUICK_HOSTED=1 \
|
|
81
|
+
npx --yes @aporthq/aport-agent-guardrails claude-code --non-interactive
|
|
82
|
+
```
|
|
83
|
+
|
|
60
84
|
### Why Developers and teams trust APort
|
|
61
85
|
|
|
62
86
|
- **Deterministic enforcement:** runtime hook, not prompt instructions
|
|
@@ -159,11 +183,11 @@ aport setup --framework=langchain
|
|
|
159
183
|
```
|
|
160
184
|
Then install the framework-specific Python package and follow the printed integration step for your framework.
|
|
161
185
|
|
|
162
|
-
This runs
|
|
186
|
+
This runs setup and writes config for your framework. Choose hosted setup for passport and setup-key creation, or local setup for an on-disk passport. Follow the **next steps** printed at the end (e.g. restart Cursor; or for CrewAI: by default install `aport-agent-guardrails-crewai` for released CrewAI, or opt into native-provider mode if your CrewAI build supports it).
|
|
163
187
|
|
|
164
188
|
**Guardrail mode (local vs API)** — On the **Node** installer (`npx @aporthq/aport-agent-guardrails …` / `bin/agent-guardrails`), every framework accepts the same flags: `--mode=api` (with optional `--api-url`, default `https://api.aport.io`) or `--mode=local`, and an optional hosted `ap_<hex>` argument (API mode, no local passport). That flow writes `…/aport/guardrail-mode.env` where the hooks/generic installers need it. The **Python** `aport setup` CLI does not parse those flags yet; use the Node command above for API/local mode during setup, or set mode in your framework `config.yaml` per the framework doc.
|
|
165
189
|
|
|
166
|
-
**2. Hosted passport (optional)** — If you already have an agent_id from [aport.io](https://aport.io), use it to skip
|
|
190
|
+
**2. Hosted passport (optional)** — The installer can create a hosted passport during setup. If you already have an agent_id from [aport.io](https://aport.io), use it to skip passport creation: `npx @aporthq/aport-agent-guardrails claude-code <agent_id>`. See [Hosted passport setup](docs/HOSTED_PASSPORT_SETUP.md).
|
|
167
191
|
|
|
168
192
|
**3. Test that policy runs** — After setup, the guardrail runs automatically when your agent uses tools (Cursor hook, LangChain callback, OpenClaw plugin, etc.). To try allow/deny from the command line (any framework), use the installed `aport-guardrail` command (Node) or call the evaluator from Python; both use your existing passport from the framework config dir (e.g. `~/.cursor/aport/`, `~/.aport/langchain/aport/`).
|
|
169
193
|
|
|
@@ -445,7 +469,7 @@ Use the framework-specific doc for where config and passport live and for any ex
|
|
|
445
469
|
|
|
446
470
|
| Doc | Description |
|
|
447
471
|
|-----|-------------|
|
|
448
|
-
| [QuickStart: OpenClaw Plugin](docs/QUICKSTART_OPENCLAW_PLUGIN.md) |
|
|
472
|
+
| [QuickStart: OpenClaw Plugin](docs/QUICKSTART_OPENCLAW_PLUGIN.md) | One-command OpenClaw setup |
|
|
449
473
|
| [Hosted passport setup](docs/HOSTED_PASSPORT_SETUP.md) | Use passport from aport.io — `npx ... openclaw <agent_id>` or choose hosted in wizard |
|
|
450
474
|
| [Verification methods (local vs API)](docs/VERIFICATION_METHODS.md) | Deep dive: bash vs API evaluator |
|
|
451
475
|
| [Quick Start Guide](docs/QUICKSTART.md) | Passport wizard, copy-paste option |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# APort Claude Code hook: reads tool_name + tool_input from JSON stdin
|
|
2
|
+
# APort Claude Code hook: reads tool_name + tool_input from JSON stdin (path-based Read uses guardrail).
|
|
3
3
|
# maps to APort policy, calls guardrail, outputs hookSpecificOutput deny or exit 0.
|
|
4
4
|
# Exit 0 = allow, exit 2 = block. Other exits = hook error (Claude Code may fail-open).
|
|
5
5
|
# Output format: Claude Code official schema (hookSpecificOutput.permissionDecision), NOT Cursor format.
|
|
@@ -9,12 +9,19 @@ set -e
|
|
|
9
9
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
10
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
11
11
|
|
|
12
|
+
# Anchor data paths to Claude Code config before resolve (hosted/API installs may have no passport.json).
|
|
13
|
+
# shellcheck source=bin/lib/framework-hook-paths.sh
|
|
14
|
+
. "$ROOT_DIR/bin/lib/framework-hook-paths.sh"
|
|
15
|
+
aport_hook_prepare_framework_paths "claude-code" "${APORT_CLAUDE_CODE_CONFIG_DIR:-}" "$HOME/.claude"
|
|
16
|
+
|
|
12
17
|
# Path resolver: probes ~/.claude, ~/.cursor, ~/.openclaw, etc.
|
|
13
18
|
# shellcheck source=bin/aport-resolve-paths.sh
|
|
14
19
|
. "$ROOT_DIR/bin/aport-resolve-paths.sh"
|
|
15
20
|
# shellcheck source=bin/lib/guardrail-mode.sh
|
|
16
21
|
. "$ROOT_DIR/bin/lib/guardrail-mode.sh"
|
|
17
|
-
|
|
22
|
+
# shellcheck source=bin/lib/hook-read-policy.sh
|
|
23
|
+
. "$ROOT_DIR/bin/lib/hook-read-policy.sh"
|
|
24
|
+
load_guardrail_mode_for_hooks "${APORT_CONFIG_DIR:-${OPENCLAW_CONFIG_DIR:-$HOME/.claude}}"
|
|
18
25
|
|
|
19
26
|
GUARDRAIL="$ROOT_DIR/bin/aport-guardrail-bash.sh"
|
|
20
27
|
if [ "${APORT_GUARDRAIL_MODE:-local}" = "api" ]; then
|
|
@@ -49,7 +56,8 @@ set -e
|
|
|
49
56
|
if [ "$JQ_EXIT" -ne 0 ] || [ -z "$TOOL_NAME" ]; then
|
|
50
57
|
TOOL_NAME="unknown"
|
|
51
58
|
fi
|
|
52
|
-
|
|
59
|
+
# Strip permission-rule specifiers (e.g. Agent(Explore) -> Agent) before normalization.
|
|
60
|
+
TOOL_NAME_NORM="$(printf '%s' "$TOOL_NAME" | tr -d '[:space:]' | sed 's/^functions\.//' | sed 's/(.*$//' | tr '[:upper:]' '[:lower:]')"
|
|
53
61
|
set +e
|
|
54
62
|
TOOL_INPUT="$(echo "$INPUT" | jq -c '.tool_input // {}' 2> /dev/null)"
|
|
55
63
|
JQ_EXIT=$?
|
|
@@ -81,40 +89,45 @@ GUARDRAIL_TOOL=""
|
|
|
81
89
|
CONTEXT_JSON="{}"
|
|
82
90
|
|
|
83
91
|
case "$TOOL_NAME_NORM" in
|
|
84
|
-
bash | shell)
|
|
92
|
+
bash | shell | powershell | monitor)
|
|
85
93
|
GUARDRAIL_TOOL="bash"
|
|
86
|
-
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{command: (.command // "")}')"
|
|
94
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{command: (.command // .script // "")}')"
|
|
95
|
+
;;
|
|
96
|
+
read | readfile | semanticsearch)
|
|
97
|
+
if ! aport_hook_try_read_evaluation "$TOOL_NAME_NORM" "$TOOL_INPUT"; then
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
87
100
|
;;
|
|
88
|
-
|
|
89
|
-
#
|
|
101
|
+
glob | ls | grep | lsp | todoread | toolsearch | askuserquestion | listmcpresourcestool | readmcpresourcetool | waitformcpservers)
|
|
102
|
+
# Search/list/read tools without a single file_path: allow without evaluator
|
|
90
103
|
exit 0
|
|
91
104
|
;;
|
|
92
|
-
taskget | tasklist | taskoutput | cronlist)
|
|
93
|
-
# Read-only task/cron queries: allow without evaluator
|
|
105
|
+
taskget | tasklist | taskoutput | cronlist | schedulewakeup | pushnotification)
|
|
106
|
+
# Read-only task/cron queries and notifications: allow without evaluator
|
|
94
107
|
exit 0
|
|
95
108
|
;;
|
|
96
109
|
enterplanmode | exitplanmode)
|
|
97
110
|
# Internal state transitions: allow without evaluator
|
|
98
111
|
exit 0
|
|
99
112
|
;;
|
|
100
|
-
write | edit | multiedit | notebookedit | todowrite | delete | strreplace | editnotebook)
|
|
113
|
+
write | edit | multiedit | notebookedit | todowrite | delete | strreplace | editnotebook | shareonboardingguide)
|
|
101
114
|
GUARDRAIL_TOOL="write"
|
|
102
115
|
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{file_path: (.file_path // .path // "")}')"
|
|
103
116
|
;;
|
|
104
117
|
websearch | webfetch)
|
|
105
|
-
GUARDRAIL_TOOL="
|
|
106
|
-
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // .query // "")}')"
|
|
118
|
+
GUARDRAIL_TOOL="websearch"
|
|
119
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // ""), query: (.query // "")}')"
|
|
107
120
|
;;
|
|
108
121
|
browser)
|
|
109
122
|
GUARDRAIL_TOOL="browser"
|
|
110
123
|
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // "")}')"
|
|
111
124
|
;;
|
|
112
|
-
task | taskcreate | taskupdate | taskstop |
|
|
125
|
+
agent | task | taskcreate | taskupdate | taskstop | skill | enterworktree | exitworktree | subagent | subagentstart | sendmessage | teamcreate | teamdelete | remotetrigger)
|
|
113
126
|
GUARDRAIL_TOOL="session.create"
|
|
114
|
-
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .prompt // "")}')"
|
|
127
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .prompt // .task // .message // ""), subagent_type: (.subagent_type // .agent_type // "")}')"
|
|
115
128
|
;;
|
|
116
129
|
croncreate | crondelete)
|
|
117
|
-
GUARDRAIL_TOOL="
|
|
130
|
+
GUARDRAIL_TOOL="session.create"
|
|
118
131
|
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .schedule // "")}')"
|
|
119
132
|
;;
|
|
120
133
|
mcp__* | mcp:* | callmcptool)
|
|
@@ -128,12 +141,21 @@ case "$TOOL_NAME_NORM" in
|
|
|
128
141
|
esac
|
|
129
142
|
|
|
130
143
|
# Use a per-invocation decision file to avoid race conditions with concurrent tool calls
|
|
131
|
-
HOOK_DECISION_FILE="${OPENCLAW_DECISION_FILE:-}"
|
|
144
|
+
HOOK_DECISION_FILE="${APORT_DECISION_FILE:-${OPENCLAW_DECISION_FILE:-}}"
|
|
132
145
|
if [ -n "$HOOK_DECISION_FILE" ]; then
|
|
133
146
|
HOOK_DECISION_FILE="${HOOK_DECISION_FILE%.json}-$$.json"
|
|
147
|
+
export APORT_DECISION_FILE="$HOOK_DECISION_FILE"
|
|
134
148
|
export OPENCLAW_DECISION_FILE="$HOOK_DECISION_FILE"
|
|
135
149
|
fi
|
|
136
150
|
|
|
151
|
+
# Read tools: send only file_path to the evaluator (Claude may attach large file bodies in tool_input).
|
|
152
|
+
if [ "$GUARDRAIL_TOOL" = "read" ]; then
|
|
153
|
+
CONTEXT_JSON="$(printf '%s' "$CONTEXT_JSON" | jq -c '{file_path: (.file_path // .path // "")}' 2> /dev/null || echo '{"file_path":""}')"
|
|
154
|
+
if [ -z "$(printf '%s' "$CONTEXT_JSON" | jq -r '.file_path // ""' 2> /dev/null)" ]; then
|
|
155
|
+
exit 0
|
|
156
|
+
fi
|
|
157
|
+
fi
|
|
158
|
+
|
|
137
159
|
# Call core evaluator (guardrail expects tool name, not policy ID)
|
|
138
160
|
set +e
|
|
139
161
|
"$GUARDRAIL" "$GUARDRAIL_TOOL" "$CONTEXT_JSON" 2> /dev/null
|
|
@@ -45,8 +45,9 @@ fi
|
|
|
45
45
|
PASSPORT_FILE="${PASSPORT_FILE/#\~/$HOME}"
|
|
46
46
|
|
|
47
47
|
# Config dir: from env, or derived from passport path (e.g. .../aport/passport.json -> parent of aport)
|
|
48
|
-
if [ -n "${OPENCLAW_CONFIG_DIR:-}" ]; then
|
|
49
|
-
CONFIG_DIR="${OPENCLAW_CONFIG_DIR
|
|
48
|
+
if [ -n "${APORT_CONFIG_DIR:-${OPENCLAW_CONFIG_DIR:-}}" ]; then
|
|
49
|
+
CONFIG_DIR="${APORT_CONFIG_DIR:-${OPENCLAW_CONFIG_DIR:-}}"
|
|
50
|
+
CONFIG_DIR="${CONFIG_DIR/#\~/$HOME}"
|
|
50
51
|
else
|
|
51
52
|
CONFIG_DIR="$(dirname "$PASSPORT_FILE")"
|
|
52
53
|
case "$PASSPORT_FILE" in
|
package/bin/aport-cursor-hook.sh
CHANGED
|
@@ -13,12 +13,19 @@ set -e
|
|
|
13
13
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
14
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
15
15
|
|
|
16
|
+
# Anchor data paths to Cursor config before resolve (hosted/API installs may have no passport.json).
|
|
17
|
+
# shellcheck source=bin/lib/framework-hook-paths.sh
|
|
18
|
+
. "$ROOT_DIR/bin/lib/framework-hook-paths.sh"
|
|
19
|
+
aport_hook_prepare_framework_paths "cursor" "${APORT_CURSOR_CONFIG_DIR:-}" "$HOME/.cursor"
|
|
20
|
+
|
|
16
21
|
# Passport/config: resolver probes ~/.cursor, ~/.openclaw, ~/.aport/*, etc.
|
|
17
22
|
# shellcheck source=bin/aport-resolve-paths.sh
|
|
18
23
|
. "$ROOT_DIR/bin/aport-resolve-paths.sh"
|
|
19
24
|
# shellcheck source=bin/lib/guardrail-mode.sh
|
|
20
25
|
. "$ROOT_DIR/bin/lib/guardrail-mode.sh"
|
|
21
|
-
|
|
26
|
+
# shellcheck source=bin/lib/hook-read-policy.sh
|
|
27
|
+
. "$ROOT_DIR/bin/lib/hook-read-policy.sh"
|
|
28
|
+
load_guardrail_mode_for_hooks "${APORT_CONFIG_DIR:-${OPENCLAW_CONFIG_DIR:-$HOME/.cursor}}"
|
|
22
29
|
|
|
23
30
|
GUARDRAIL="$ROOT_DIR/bin/aport-guardrail-bash.sh"
|
|
24
31
|
if [ "${APORT_GUARDRAIL_MODE:-local}" = "api" ]; then
|
|
@@ -82,8 +89,10 @@ HOOK_EVENT="$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2> /dev/null)"
|
|
|
82
89
|
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // ""' 2> /dev/null)"
|
|
83
90
|
|
|
84
91
|
if [ "$HOOK_EVENT" = "beforeReadFile" ] || { [ -z "$HOOK_EVENT" ] && [ -z "$TOOL_NAME" ] && echo "$INPUT" | jq -e '.file_path and .content' &> /dev/null; }; then
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
FILE_PATH="$(echo "$INPUT" | jq -r '.file_path // ""' 2> /dev/null || true)"
|
|
93
|
+
if ! aport_hook_try_read_evaluation_from_file_path "$FILE_PATH"; then
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
87
96
|
|
|
88
97
|
elif [ "$HOOK_EVENT" = "subagentStart" ] || { [ -z "$HOOK_EVENT" ] && echo "$INPUT" | jq -e '.subagent_id' &> /dev/null; }; then
|
|
89
98
|
# subagentStart: sub-agent spawning
|
|
@@ -96,36 +105,48 @@ elif [ "$HOOK_EVENT" = "beforeMCPExecution" ] || { [ -n "$TOOL_NAME" ] && echo "
|
|
|
96
105
|
CONTEXT_JSON="$(safe_jq "$INPUT" '{tool_name: (.tool_name // ""), tool_input: (.tool_input // {})}')"
|
|
97
106
|
|
|
98
107
|
elif [ -n "$TOOL_NAME" ]; then
|
|
99
|
-
# preToolUse:
|
|
100
|
-
#
|
|
101
|
-
TOOL_NORM="$(
|
|
108
|
+
# preToolUse: Shell, Read, Write, Grep, Delete, Task, WebSearch, Agent, MCP:*, etc.
|
|
109
|
+
# See docs/FRAMEWORK_TOOL_MAPPING_AUDIT.md and https://cursor.com/docs/agent/hooks
|
|
110
|
+
TOOL_NORM="$(printf '%s' "$TOOL_NAME" | tr -d '[:space:]' | sed 's/^functions\.//' | sed 's/(.*$//' | tr '[:upper:]' '[:lower:]')"
|
|
102
111
|
case "$TOOL_NORM" in
|
|
103
|
-
shell)
|
|
112
|
+
shell | bash)
|
|
104
113
|
GUARDRAIL_TOOL="bash"
|
|
105
114
|
CONTEXT_JSON="$(safe_jq "$INPUT" '{command: (.tool_input.command // "")}')"
|
|
106
115
|
;;
|
|
107
|
-
read |
|
|
108
|
-
|
|
116
|
+
read | readfile | semanticsearch)
|
|
117
|
+
TOOL_INPUT="$(safe_jq "$INPUT" '.tool_input // {}')"
|
|
118
|
+
if ! aport_hook_try_read_evaluation "$TOOL_NORM" "$TOOL_INPUT"; then
|
|
119
|
+
exit 0
|
|
120
|
+
fi
|
|
121
|
+
;;
|
|
122
|
+
grep | glob | ls | lsp | listmcpresourcestool | readmcpresourcetool | toolsearch | waitformcpservers | taskget | tasklist | taskoutput | cronlist)
|
|
109
123
|
exit 0
|
|
110
124
|
;;
|
|
111
|
-
write | strreplace | editnotebook)
|
|
125
|
+
write | strreplace | edit | multiedit | editnotebook | applypatch | notebookedit | delete)
|
|
112
126
|
GUARDRAIL_TOOL="write"
|
|
113
127
|
CONTEXT_JSON="$(safe_jq "$INPUT" '{file_path: (.tool_input.file_path // .tool_input.path // "")}')"
|
|
114
128
|
;;
|
|
115
|
-
|
|
116
|
-
GUARDRAIL_TOOL="
|
|
117
|
-
CONTEXT_JSON="$(safe_jq "$INPUT" '{
|
|
129
|
+
websearch | webfetch)
|
|
130
|
+
GUARDRAIL_TOOL="websearch"
|
|
131
|
+
CONTEXT_JSON="$(safe_jq "$INPUT" '{url: (.tool_input.url // ""), query: (.tool_input.query // "")}')"
|
|
118
132
|
;;
|
|
119
|
-
|
|
133
|
+
browser)
|
|
134
|
+
GUARDRAIL_TOOL="browser"
|
|
135
|
+
CONTEXT_JSON="$(safe_jq "$INPUT" '{url: (.tool_input.url // "")}')"
|
|
136
|
+
;;
|
|
137
|
+
task | agent | taskcreate | taskupdate | taskstop | skill | subagent | subagentstart | sendmessage | teamcreate | teamdelete)
|
|
120
138
|
GUARDRAIL_TOOL="session.create"
|
|
121
139
|
CONTEXT_JSON="$(safe_jq "$INPUT" '{description: (.tool_input.description // .tool_input.prompt // "")}')"
|
|
122
140
|
;;
|
|
123
|
-
|
|
141
|
+
croncreate | crondelete)
|
|
142
|
+
GUARDRAIL_TOOL="session.create"
|
|
143
|
+
CONTEXT_JSON="$(safe_jq "$INPUT" '{description: (.tool_input.description // .tool_input.schedule // "")}')"
|
|
144
|
+
;;
|
|
145
|
+
mcp__* | mcp:* | callmcptool)
|
|
124
146
|
GUARDRAIL_TOOL="mcp.tool"
|
|
125
147
|
CONTEXT_JSON="$(safe_jq "$INPUT" '{tool_name: (.tool_name // ""), tool_input: (.tool_input // {})}')"
|
|
126
148
|
;;
|
|
127
149
|
*)
|
|
128
|
-
# Unknown preToolUse tool: fail-closed
|
|
129
150
|
deny "🛡️ APort: unknown tool '$TOOL_NAME' — fail-closed policy"
|
|
130
151
|
;;
|
|
131
152
|
esac
|
|
@@ -148,12 +169,22 @@ else
|
|
|
148
169
|
fi
|
|
149
170
|
|
|
150
171
|
# Use a per-invocation decision file to avoid race conditions with concurrent tool calls
|
|
151
|
-
HOOK_DECISION_FILE="${OPENCLAW_DECISION_FILE:-}"
|
|
172
|
+
HOOK_DECISION_FILE="${APORT_DECISION_FILE:-${OPENCLAW_DECISION_FILE:-}}"
|
|
152
173
|
if [ -n "$HOOK_DECISION_FILE" ]; then
|
|
153
174
|
HOOK_DECISION_FILE="${HOOK_DECISION_FILE%.json}-$$.json"
|
|
175
|
+
export APORT_DECISION_FILE="$HOOK_DECISION_FILE"
|
|
154
176
|
export OPENCLAW_DECISION_FILE="$HOOK_DECISION_FILE"
|
|
155
177
|
fi
|
|
156
178
|
|
|
179
|
+
# Read tools: send only file_path to the evaluator (Cursor may attach large file bodies in tool_input).
|
|
180
|
+
if [ "$GUARDRAIL_TOOL" = "read" ]; then
|
|
181
|
+
CONTEXT_JSON="$(printf '%s' "$CONTEXT_JSON" | jq -c '{file_path: (.file_path // .path // "")}' 2> /dev/null || echo '{"file_path":""}')"
|
|
182
|
+
if [ -z "$(printf '%s' "$CONTEXT_JSON" | jq -r '.file_path // ""' 2> /dev/null)" ]; then
|
|
183
|
+
echo '{"permission":"allow","allowed":true}'
|
|
184
|
+
exit 0
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
|
|
157
188
|
# Call core evaluator
|
|
158
189
|
set +e
|
|
159
190
|
"$GUARDRAIL" "$GUARDRAIL_TOOL" "$CONTEXT_JSON" 2> /dev/null
|
|
@@ -177,7 +208,7 @@ if [ -n "$HOOK_DECISION_FILE" ] && [ -f "$HOOK_DECISION_FILE" ]; then
|
|
|
177
208
|
fi
|
|
178
209
|
# Fallback: try common config dirs
|
|
179
210
|
if [ "$REASON" = "Policy denied this action." ]; then
|
|
180
|
-
for DEC in "${OPENCLAW_CONFIG_DIR:-$HOME/.cursor}/aport/decision.json" "$HOME/.cursor/aport/decision.json" "$HOME/.openclaw/aport/decision.json"; do
|
|
211
|
+
for DEC in "${APORT_CONFIG_DIR:-${OPENCLAW_CONFIG_DIR:-$HOME/.cursor}}/aport/decision.json" "$HOME/.cursor/aport/decision.json" "$HOME/.openclaw/aport/decision.json"; do
|
|
181
212
|
if [ -f "$DEC" ]; then
|
|
182
213
|
R="$(jq -r '.reasons[0].message // empty' "$DEC" 2> /dev/null)"
|
|
183
214
|
[ -n "$R" ] && REASON="$R" && break
|
|
@@ -63,6 +63,8 @@ if [ -n "$DEBUG_APORT" ]; then
|
|
|
63
63
|
fi
|
|
64
64
|
|
|
65
65
|
# Export environment variables for evaluator (APORT_API_URL, APORT_AGENT_ID, APORT_API_KEY passed through)
|
|
66
|
+
export APORT_PASSPORT_FILE="$PASSPORT_FILE"
|
|
67
|
+
export APORT_DECISION_FILE="$DECISION_FILE"
|
|
66
68
|
export OPENCLAW_PASSPORT_FILE="$PASSPORT_FILE"
|
|
67
69
|
export OPENCLAW_DECISION_FILE="$DECISION_FILE"
|
|
68
70
|
|
|
@@ -301,6 +301,17 @@ else
|
|
|
301
301
|
LIMITS=$(echo "$PASSPORT" | jq ".limits.\"$POLICY_BASE\" // {}")
|
|
302
302
|
fi
|
|
303
303
|
|
|
304
|
+
is_default_sensitive_read_path() {
|
|
305
|
+
local path_lower
|
|
306
|
+
path_lower="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
|
|
307
|
+
case "$path_lower" in
|
|
308
|
+
.env* | */.env* | .aws/* | */.aws/* | .ssh/* | */.ssh/* | *credentials* | *id_rsa* | *id_dsa* | *id_ecdsa* | *id_ed25519* | *.pem | *.key | *password* | .gnupg/* | */.gnupg/* | .kube/* | */.kube/*)
|
|
309
|
+
return 0
|
|
310
|
+
;;
|
|
311
|
+
esac
|
|
312
|
+
return 1
|
|
313
|
+
}
|
|
314
|
+
|
|
304
315
|
# Evaluate policy-specific limits
|
|
305
316
|
if [[ "$POLICY_ID" == "code.repository.merge"* ]]; then
|
|
306
317
|
FILES_CHANGED=$(echo "$CONTEXT_JSON" | jq -r '.files_changed // .files // 0')
|
|
@@ -426,6 +437,10 @@ fi
|
|
|
426
437
|
if [[ "$POLICY_ID" == "data.file.read.v1" ]]; then
|
|
427
438
|
FILE_PATH=$(echo "$CONTEXT_JSON" | jq -r '.file_path // .path // ""')
|
|
428
439
|
if [ -n "$FILE_PATH" ]; then
|
|
440
|
+
if is_default_sensitive_read_path "$FILE_PATH"; then
|
|
441
|
+
write_decision false "$POLICY_ID" "oap.blocked_pattern" "File path matches default sensitive read pattern"
|
|
442
|
+
fi
|
|
443
|
+
|
|
429
444
|
# Check allowed paths
|
|
430
445
|
PATH_ALLOWED=false
|
|
431
446
|
while IFS= read -r allowed_path; do
|
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
# - aport-guardrail-api.sh
|
|
6
6
|
# - aport-status.sh
|
|
7
7
|
# (aport-create-passport.sh uses --output; wrappers set env so children get resolved paths.)
|
|
8
|
-
# Sets
|
|
9
|
-
#
|
|
8
|
+
# Sets canonical APORT_* path variables plus legacy OPENCLAW_* aliases for compatibility.
|
|
9
|
+
# Also sets PASSPORT_FILE, DECISION_FILE, AUDIT_LOG. Passport status (active|suspended|revoked)
|
|
10
|
+
# is source of truth for suspend; no separate file.
|
|
10
11
|
# Caller must ensure APORT_DATA_DIR exists before writing (e.g. mkdir -p "$(dirname "$AUDIT_LOG")").
|
|
11
12
|
|
|
12
13
|
resolve_aport_paths() {
|
|
13
14
|
local config_dir
|
|
14
15
|
local passport_path
|
|
15
16
|
local data_dir
|
|
17
|
+
local explicit_passport="${APORT_PASSPORT_FILE:-${OPENCLAW_PASSPORT_FILE:-}}"
|
|
18
|
+
local explicit_decision="${APORT_DECISION_FILE:-${OPENCLAW_DECISION_FILE:-}}"
|
|
19
|
+
local explicit_audit="${APORT_AUDIT_LOG:-${OPENCLAW_AUDIT_LOG:-}}"
|
|
20
|
+
local explicit_config="${APORT_CONFIG_DIR:-${OPENCLAW_CONFIG_DIR:-}}"
|
|
16
21
|
|
|
17
22
|
# Source validation if available (for validate_passport_path)
|
|
18
23
|
local _resolve_script_dir
|
|
@@ -24,9 +29,14 @@ resolve_aport_paths() {
|
|
|
24
29
|
. "$_resolve_script_dir/../bin/lib/validation.sh" 2> /dev/null || true
|
|
25
30
|
fi
|
|
26
31
|
|
|
32
|
+
[ -n "$explicit_passport" ] && explicit_passport="${explicit_passport/#\~/$HOME}"
|
|
33
|
+
[ -n "$explicit_decision" ] && explicit_decision="${explicit_decision/#\~/$HOME}"
|
|
34
|
+
[ -n "$explicit_audit" ] && explicit_audit="${explicit_audit/#\~/$HOME}"
|
|
35
|
+
[ -n "$explicit_config" ] && explicit_config="${explicit_config/#\~/$HOME}"
|
|
36
|
+
|
|
27
37
|
# 0) AGENTS.md enforcement block (repo-scoped, highest priority after explicit env)
|
|
28
|
-
# Only checked when no explicit env var is set
|
|
29
|
-
if [ -z "$
|
|
38
|
+
# Only checked when no explicit passport env var is set.
|
|
39
|
+
if [ -z "$explicit_passport" ]; then
|
|
30
40
|
local _agentsmd_lib="$_resolve_script_dir/lib/agentsmd.sh"
|
|
31
41
|
[ ! -f "$_agentsmd_lib" ] && _agentsmd_lib="$_resolve_script_dir/../bin/lib/agentsmd.sh"
|
|
32
42
|
if [ -f "$_agentsmd_lib" ]; then
|
|
@@ -39,53 +49,61 @@ resolve_aport_paths() {
|
|
|
39
49
|
if [ -n "$AGENTSMD_PASSPORT" ] && [ -f "$AGENTSMD_PASSPORT" ]; then
|
|
40
50
|
passport_path="$AGENTSMD_PASSPORT"
|
|
41
51
|
data_dir="$(dirname "$AGENTSMD_PASSPORT")"
|
|
52
|
+
explicit_decision="${explicit_decision:-${data_dir}/decision.json}"
|
|
53
|
+
explicit_audit="${explicit_audit:-${data_dir}/audit.log}"
|
|
54
|
+
|
|
55
|
+
export APORT_PASSPORT_FILE="$passport_path"
|
|
42
56
|
export OPENCLAW_PASSPORT_FILE="$passport_path"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
export OPENCLAW_AUDIT_LOG="$
|
|
57
|
+
export APORT_DECISION_FILE="$explicit_decision"
|
|
58
|
+
export OPENCLAW_DECISION_FILE="$explicit_decision"
|
|
59
|
+
export APORT_AUDIT_LOG="$explicit_audit"
|
|
60
|
+
export OPENCLAW_AUDIT_LOG="$explicit_audit"
|
|
61
|
+
|
|
47
62
|
PASSPORT_FILE="$passport_path"
|
|
48
|
-
DECISION_FILE="$
|
|
49
|
-
AUDIT_LOG="$
|
|
63
|
+
DECISION_FILE="$explicit_decision"
|
|
64
|
+
AUDIT_LOG="$explicit_audit"
|
|
50
65
|
return 0
|
|
51
66
|
fi
|
|
52
67
|
fi
|
|
53
68
|
fi
|
|
54
69
|
fi
|
|
55
70
|
|
|
56
|
-
# 1) Explicit path set and file exists
|
|
57
|
-
if [ -n "$
|
|
71
|
+
# 1) Explicit path set and file exists -> use it (plugin or wrapper)
|
|
72
|
+
if [ -n "$explicit_passport" ] && [ -f "$explicit_passport" ]; then
|
|
58
73
|
# Validate env-provided path if validator is available
|
|
59
74
|
if type validate_explicit_passport_path &> /dev/null; then
|
|
60
|
-
if ! validate_explicit_passport_path "$
|
|
61
|
-
echo "[aport] WARN:
|
|
75
|
+
if ! validate_explicit_passport_path "$explicit_passport"; then
|
|
76
|
+
echo "[aport] WARN: APORT_PASSPORT_FILE path failed validation: $explicit_passport" >&2
|
|
62
77
|
fi
|
|
63
78
|
fi
|
|
64
|
-
data_dir="$(dirname "$
|
|
65
|
-
passport_path="$
|
|
66
|
-
# 2) Explicit path set but file missing
|
|
67
|
-
elif [ -n "$
|
|
68
|
-
config_dir="$(cd "$(dirname "$
|
|
79
|
+
data_dir="$(dirname "$explicit_passport")"
|
|
80
|
+
passport_path="$explicit_passport"
|
|
81
|
+
# 2) Explicit path set but file missing -> legacy: try parent dir (e.g. .../openclaw/passport.json)
|
|
82
|
+
elif [ -n "$explicit_passport" ]; then
|
|
83
|
+
config_dir="$(cd "$(dirname "$explicit_passport")/.." 2> /dev/null && pwd)"
|
|
69
84
|
if [ -f "${config_dir}/passport.json" ]; then
|
|
70
85
|
passport_path="${config_dir}/passport.json"
|
|
71
86
|
data_dir="$config_dir"
|
|
72
87
|
else
|
|
73
|
-
passport_path="$
|
|
74
|
-
data_dir="$(dirname "$
|
|
88
|
+
passport_path="$explicit_passport"
|
|
89
|
+
data_dir="$(dirname "$explicit_passport")"
|
|
75
90
|
fi
|
|
76
|
-
# 3)
|
|
91
|
+
# 3) Probe framework paths, or honor APORT_CONFIG_DIR / OPENCLAW_CONFIG_DIR set by framework hooks.
|
|
77
92
|
else
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
if [ -n "$explicit_config" ]; then
|
|
94
|
+
config_dir="$explicit_config"
|
|
95
|
+
else
|
|
96
|
+
config_dir=""
|
|
97
|
+
for candidate in "$HOME/.claude" "$HOME/.cursor" "$HOME/.openclaw" "$HOME/.aport/langchain" "$HOME/.aport/crewai" "$HOME/.aport/deerflow" "$HOME/.n8n"; do
|
|
98
|
+
if [ -f "${candidate}/aport/passport.json" ]; then
|
|
99
|
+
config_dir="$candidate"
|
|
100
|
+
break
|
|
101
|
+
fi
|
|
102
|
+
done
|
|
103
|
+
if [ -z "$config_dir" ]; then
|
|
104
|
+
config_dir="$HOME/.openclaw"
|
|
83
105
|
fi
|
|
84
|
-
done
|
|
85
|
-
if [ -z "$config_dir" ]; then
|
|
86
|
-
config_dir="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
|
|
87
106
|
fi
|
|
88
|
-
config_dir="${config_dir/#\~/$HOME}"
|
|
89
107
|
if [ -f "${config_dir}/aport/passport.json" ]; then
|
|
90
108
|
passport_path="${config_dir}/aport/passport.json"
|
|
91
109
|
data_dir="${config_dir}/aport"
|
|
@@ -98,19 +116,28 @@ resolve_aport_paths() {
|
|
|
98
116
|
fi
|
|
99
117
|
fi
|
|
100
118
|
|
|
119
|
+
export APORT_PASSPORT_FILE="$passport_path"
|
|
101
120
|
export OPENCLAW_PASSPORT_FILE="$passport_path"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
|
|
122
|
+
# Preserve explicitly set decision/audit paths (e.g. tests); otherwise use data_dir.
|
|
123
|
+
if [ -n "$explicit_decision" ]; then
|
|
124
|
+
export APORT_DECISION_FILE="$explicit_decision"
|
|
125
|
+
else
|
|
126
|
+
export APORT_DECISION_FILE="${data_dir}/decision.json"
|
|
127
|
+
fi
|
|
128
|
+
export OPENCLAW_DECISION_FILE="$APORT_DECISION_FILE"
|
|
129
|
+
|
|
130
|
+
if [ -n "$explicit_audit" ]; then
|
|
131
|
+
export APORT_AUDIT_LOG="$explicit_audit"
|
|
105
132
|
else
|
|
106
|
-
export
|
|
133
|
+
export APORT_AUDIT_LOG="${data_dir}/audit.log"
|
|
107
134
|
fi
|
|
108
|
-
export OPENCLAW_AUDIT_LOG="$
|
|
135
|
+
export OPENCLAW_AUDIT_LOG="$APORT_AUDIT_LOG"
|
|
109
136
|
|
|
110
|
-
PASSPORT_FILE="$
|
|
111
|
-
DECISION_FILE="$
|
|
112
|
-
AUDIT_LOG="$
|
|
137
|
+
PASSPORT_FILE="$APORT_PASSPORT_FILE"
|
|
138
|
+
DECISION_FILE="$APORT_DECISION_FILE"
|
|
139
|
+
AUDIT_LOG="$APORT_AUDIT_LOG"
|
|
113
140
|
}
|
|
114
141
|
|
|
115
|
-
# When sourced, resolve immediately so callers just use the vars
|
|
142
|
+
# When sourced, resolve immediately so callers just use the vars.
|
|
116
143
|
resolve_aport_paths
|
|
@@ -14,6 +14,8 @@ source "$LIB/config.sh"
|
|
|
14
14
|
source "$LIB/framework-setup.sh"
|
|
15
15
|
# shellcheck source=../lib/guardrail-mode.sh
|
|
16
16
|
source "$LIB/guardrail-mode.sh"
|
|
17
|
+
# shellcheck source=../lib/quick-hosted.sh
|
|
18
|
+
source "$LIB/quick-hosted.sh"
|
|
17
19
|
|
|
18
20
|
run_setup() {
|
|
19
21
|
parse_guardrail_mode_args "$@"
|
|
@@ -28,6 +30,9 @@ run_setup() {
|
|
|
28
30
|
hosted_agent_id="$APORT_HOSTED_AGENT_ID_CLI"
|
|
29
31
|
export APORT_AGENT_ID="$hosted_agent_id"
|
|
30
32
|
log_info "Using hosted passport (agent_id: $hosted_agent_id) — skipping wizard."
|
|
33
|
+
elif aport_maybe_configure_hosted_passport "claude-code" "$config_dir"; then
|
|
34
|
+
hosted_agent_id="$APORT_AGENT_ID"
|
|
35
|
+
log_info "Using hosted passport (agent_id: $hosted_agent_id) — skipping wizard."
|
|
31
36
|
else
|
|
32
37
|
# Check AGENTS.md for enforcement config — skip wizard if already configured
|
|
33
38
|
# shellcheck source=../lib/agentsmd.sh
|
|
@@ -65,6 +70,11 @@ run_setup() {
|
|
|
65
70
|
_write_claude_settings "$SETTINGS_FILE" "$HOOK_SCRIPT"
|
|
66
71
|
chmod 600 "$SETTINGS_FILE"
|
|
67
72
|
|
|
73
|
+
# Audit log is appended by hooks; create empty file so the advertised path exists after install.
|
|
74
|
+
mkdir -p "$config_dir/aport"
|
|
75
|
+
: >> "$config_dir/aport/audit.log"
|
|
76
|
+
chmod 600 "$config_dir/aport/audit.log" 2> /dev/null || true
|
|
77
|
+
|
|
68
78
|
echo ""
|
|
69
79
|
echo " Next steps (Claude Code):"
|
|
70
80
|
echo " ─────────────────────────"
|
|
@@ -78,7 +88,7 @@ run_setup() {
|
|
|
78
88
|
echo " 5. Restart Claude Code so the PreToolUse hook is picked up."
|
|
79
89
|
echo " 6. Tool use will be checked by APort policy (exit 2 = block)."
|
|
80
90
|
echo ""
|
|
81
|
-
echo " Audit log: $config_dir/aport/audit.log"
|
|
91
|
+
echo " Audit log: $config_dir/aport/audit.log (entries added on each policy check)"
|
|
82
92
|
echo ""
|
|
83
93
|
echo " Note: claude --dangerously-skip-permissions bypasses ALL hooks including APort."
|
|
84
94
|
echo " See: docs/frameworks/claude-code.md"
|
package/bin/frameworks/cursor.sh
CHANGED
|
@@ -14,6 +14,8 @@ source "$LIB/config.sh"
|
|
|
14
14
|
source "$LIB/framework-setup.sh"
|
|
15
15
|
# shellcheck source=../lib/guardrail-mode.sh
|
|
16
16
|
source "$LIB/guardrail-mode.sh"
|
|
17
|
+
# shellcheck source=../lib/quick-hosted.sh
|
|
18
|
+
source "$LIB/quick-hosted.sh"
|
|
17
19
|
|
|
18
20
|
run_setup() {
|
|
19
21
|
parse_guardrail_mode_args "$@"
|
|
@@ -29,6 +31,9 @@ run_setup() {
|
|
|
29
31
|
hosted_agent_id="$APORT_HOSTED_AGENT_ID_CLI"
|
|
30
32
|
export APORT_AGENT_ID="$hosted_agent_id"
|
|
31
33
|
log_info "Using hosted passport (agent_id: $hosted_agent_id) — skipping wizard."
|
|
34
|
+
elif aport_maybe_configure_hosted_passport "cursor" "$config_dir"; then
|
|
35
|
+
hosted_agent_id="$APORT_AGENT_ID"
|
|
36
|
+
log_info "Using hosted passport (agent_id: $hosted_agent_id) — skipping wizard."
|
|
32
37
|
else
|
|
33
38
|
# Check AGENTS.md for enforcement config — skip wizard if already configured
|
|
34
39
|
# shellcheck source=../lib/agentsmd.sh
|
|
@@ -102,7 +107,7 @@ run_setup() {
|
|
|
102
107
|
echo " 5. Restart Cursor (or reload window) so hooks are picked up."
|
|
103
108
|
echo " 6. Shell commands and tool use will be checked by APort policy (exit 2 = block)."
|
|
104
109
|
echo ""
|
|
105
|
-
echo "
|
|
110
|
+
echo " For other frameworks like Claude Code, use the dedicated integration: docs/frameworks"
|
|
106
111
|
echo ""
|
|
107
112
|
}
|
|
108
113
|
|