@aporthq/aport-agent-guardrails 1.0.25 → 1.0.27
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 +20 -4
- package/bin/agent-guardrails +36 -0
- package/bin/aport-claude-code-hook.sh +30 -14
- package/bin/aport-cursor-hook.sh +40 -15
- package/bin/aport-guardrail-bash.sh +15 -0
- package/bin/aport-reset-framework.sh +263 -0
- package/bin/lib/hook-read-policy.sh +61 -0
- package/bin/lib/tool-mapping.sh +5 -1
- package/docs/FRAMEWORK_TOOL_MAPPING_AUDIT.md +158 -0
- package/docs/RELEASE.md +1 -1
- package/docs/TOOL_POLICY_MAPPING.md +6 -1
- package/docs/frameworks/claude-code.md +31 -5
- 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/external/aport-spec/oap/CHANGELOG.md +18 -0
- package/external/aport-spec/oap/delegation.md +455 -0
- package/external/aport-spec/oap/oap-spec.md +1 -0
- package/package.json +1 -1
- package/python/aport_guardrails/core/tool-pack-mapping.json +111 -1
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ npx @aporthq/aport-agent-guardrails
|
|
|
55
55
|
- Choose your framework: `openclaw`, `cursor`, `claude-code`, `langchain`, `crewai`, `deerflow`, `n8n`
|
|
56
56
|
- OpenClaw direct: `npx @aporthq/aport-agent-guardrails openclaw`
|
|
57
57
|
- Hosted passport: `npx @aporthq/aport-agent-guardrails openclaw <agent_id>`
|
|
58
|
+
- Reset a framework to a clean APort state: `npx @aporthq/aport-agent-guardrails reset claude-code --yes`
|
|
58
59
|
|
|
59
60
|
### Why Developers and teams trust APort
|
|
60
61
|
|
|
@@ -129,6 +130,22 @@ npx @aporthq/aport-agent-guardrails
|
|
|
129
130
|
# --mode=local
|
|
130
131
|
```
|
|
131
132
|
|
|
133
|
+
**Reset / uninstall APort-owned wiring**
|
|
134
|
+
|
|
135
|
+
Use the same dispatcher for cleanup:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx @aporthq/aport-agent-guardrails reset claude-code --yes
|
|
139
|
+
# or
|
|
140
|
+
npx @aporthq/aport-agent-guardrails claude-code reset --yes
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Supported reset targets match the CLI-supported frameworks:
|
|
144
|
+
`openclaw`, `cursor`, `claude-code`, `langchain`, `crewai`, `deerflow`, `n8n`.
|
|
145
|
+
|
|
146
|
+
Reset removes APort-owned config and integration wiring for the selected framework.
|
|
147
|
+
When possible, unrelated user hooks are preserved.
|
|
148
|
+
|
|
132
149
|
**Python (LangChain, CrewAI, or DeerFlow):** Use the Python CLI directly via `uvx` or an installed package:
|
|
133
150
|
```bash
|
|
134
151
|
uvx --from aport-agent-guardrails aport setup --framework=langchain
|
|
@@ -183,7 +200,7 @@ Your framework doc (Cursor, OpenClaw, LangChain, CrewAI) describes where the con
|
|
|
183
200
|
| **Bypass risk** | None | High |
|
|
184
201
|
| **Recommended** | **Yes** | Only if plugin unavailable |
|
|
185
202
|
|
|
186
|
-
**Plugin (recommended):** Platform runs the guardrail before every tool; the model cannot skip it. This repo implements the **plugin (before_tool_call)** integration
|
|
203
|
+
**Plugin (recommended):** Platform runs the guardrail before every tool; the model cannot skip it. This repo implements the public **plugin (before_tool_call)** integration for OpenClaw.
|
|
187
204
|
**AGENTS.md:** Agent is *instructed* to call the guardrail; best-effort only.
|
|
188
205
|
|
|
189
206
|
---
|
|
@@ -405,6 +422,7 @@ See [Verification methods](docs/VERIFICATION_METHODS.md) for a detailed comparis
|
|
|
405
422
|
| Command | Purpose |
|
|
406
423
|
|--------|---------|
|
|
407
424
|
| `agent-guardrails` | Main entry — prompt for framework or pass one: `agent-guardrails openclaw \| cursor \| claude-code \| langchain \| crewai \| deerflow \| n8n`. Args after the framework are passed through (e.g. `agent-guardrails openclaw <agent_id>`). |
|
|
425
|
+
| `agent-guardrails reset <framework> [--yes]` | Remove APort-owned config and hook/plugin wiring for one framework. Positional form also works: `agent-guardrails <framework> reset --yes`. |
|
|
408
426
|
| `aport` | OpenClaw one-command setup (passport + plugin + wrappers). Optional: `aport <agent_id>` for hosted passport. |
|
|
409
427
|
| `aport-guardrail` | Run guardrail check from the CLI (e.g. `aport-guardrail system.command.execute '{"command":"ls"}'`). Uses passport from your framework config dir. |
|
|
410
428
|
|
|
@@ -517,7 +535,7 @@ Contributions welcome: policy packs, framework adapters, docs. See [CONTRIBUTING
|
|
|
517
535
|
|
|
518
536
|
Apache 2.0 — see [LICENSE](LICENSE).
|
|
519
537
|
|
|
520
|
-
**Open-core:** Local evaluation and CLI in this repo are open source (Apache 2.0). [api.aport.io](https://api.aport.io) is a separate product for cloud features
|
|
538
|
+
**Open-core:** Local evaluation and CLI in this repo are open source (Apache 2.0). [api.aport.io](https://api.aport.io) is a separate product for cloud features such as signed receipts, global kill switch, and team sync.
|
|
521
539
|
|
|
522
540
|
---
|
|
523
541
|
|
|
@@ -527,5 +545,3 @@ Apache 2.0 — see [LICENSE](LICENSE).
|
|
|
527
545
|
- [GitHub Issues](https://github.com/aporthq/aport-agent-guardrails/issues) · [Discussions](https://github.com/aporthq/aport-agent-guardrails/discussions)
|
|
528
546
|
|
|
529
547
|
---
|
|
530
|
-
|
|
531
|
-
<p align="center">Made with ❤️ by [Uchi](https://github.com/uchibeke/) </p>
|
package/bin/agent-guardrails
CHANGED
|
@@ -29,6 +29,7 @@ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
29
29
|
FRAMEWORKS_DIR="$SCRIPT_DIR/frameworks"
|
|
30
30
|
LIB_DIR="$SCRIPT_DIR/lib"
|
|
31
31
|
SUPPORTED_FRAMEWORKS=(openclaw langchain crewai cursor claude-code deerflow n8n)
|
|
32
|
+
reset_requested=""
|
|
32
33
|
|
|
33
34
|
framework_supported() {
|
|
34
35
|
local candidate="${1:-}"
|
|
@@ -44,6 +45,7 @@ framework_supported() {
|
|
|
44
45
|
# Parse --framework= and -f (skip detection when set)
|
|
45
46
|
framework=""
|
|
46
47
|
integration_mode=""
|
|
48
|
+
noninteractive_requested=""
|
|
47
49
|
REST=()
|
|
48
50
|
while [[ $# -gt 0 ]]; do
|
|
49
51
|
case "$1" in
|
|
@@ -73,6 +75,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
73
75
|
exit 1
|
|
74
76
|
fi
|
|
75
77
|
;;
|
|
78
|
+
--non-interactive|--noninteractive)
|
|
79
|
+
noninteractive_requested="1"
|
|
80
|
+
shift
|
|
81
|
+
;;
|
|
76
82
|
*)
|
|
77
83
|
REST+=("$1")
|
|
78
84
|
shift
|
|
@@ -80,7 +86,23 @@ while [[ $# -gt 0 ]]; do
|
|
|
80
86
|
esac
|
|
81
87
|
done
|
|
82
88
|
|
|
89
|
+
if [[ -n "$noninteractive_requested" ]]; then
|
|
90
|
+
export APORT_NONINTERACTIVE=1
|
|
91
|
+
fi
|
|
92
|
+
|
|
83
93
|
# If no framework from args, check if first REST argument is a framework name
|
|
94
|
+
if [[ -z "$framework" ]] && [[ ${#REST[@]} -gt 0 ]]; then
|
|
95
|
+
first_arg="${REST[0]}"
|
|
96
|
+
if [[ "$first_arg" == "reset" ]] && [[ ${#REST[@]} -gt 1 ]]; then
|
|
97
|
+
second_arg="${REST[1]}"
|
|
98
|
+
if framework_supported "$second_arg"; then
|
|
99
|
+
framework="$second_arg"
|
|
100
|
+
reset_requested="1"
|
|
101
|
+
REST=("${REST[@]:2}")
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
84
106
|
if [[ -z "$framework" ]] && [[ ${#REST[@]} -gt 0 ]]; then
|
|
85
107
|
first_arg="${REST[0]}"
|
|
86
108
|
# Check if first arg looks like a framework name (lowercase alphanumeric + hyphen)
|
|
@@ -93,6 +115,11 @@ if [[ -z "$framework" ]] && [[ ${#REST[@]} -gt 0 ]]; then
|
|
|
93
115
|
fi
|
|
94
116
|
fi
|
|
95
117
|
|
|
118
|
+
if [[ -n "$framework" ]] && [[ ${#REST[@]} -gt 0 ]] && [[ "${REST[0]}" == "reset" ]]; then
|
|
119
|
+
reset_requested="1"
|
|
120
|
+
REST=("${REST[@]:1}")
|
|
121
|
+
fi
|
|
122
|
+
|
|
96
123
|
# If still no framework from args, try APORT_FRAMEWORK (non-interactive) or detection
|
|
97
124
|
if [[ -z "$framework" ]]; then
|
|
98
125
|
if [[ -n "${APORT_FRAMEWORK:-}" ]]; then
|
|
@@ -165,6 +192,15 @@ fi
|
|
|
165
192
|
|
|
166
193
|
echo "[aport] Selected framework: $framework" >&2
|
|
167
194
|
|
|
195
|
+
if [[ -n "$reset_requested" ]]; then
|
|
196
|
+
reset_script="$SCRIPT_DIR/aport-reset-framework.sh"
|
|
197
|
+
if [[ ! -x "$reset_script" ]]; then
|
|
198
|
+
echo "[aport] ERROR: Reset helper not found: $reset_script" >&2
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
201
|
+
exec "$reset_script" "$framework" ${REST+"${REST[@]}"}
|
|
202
|
+
fi
|
|
203
|
+
|
|
168
204
|
if [[ -n "$integration_mode" ]]; then
|
|
169
205
|
case "$integration_mode" in
|
|
170
206
|
compat|native) ;;
|
|
@@ -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.
|
|
@@ -14,6 +14,8 @@ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
14
14
|
. "$ROOT_DIR/bin/aport-resolve-paths.sh"
|
|
15
15
|
# shellcheck source=bin/lib/guardrail-mode.sh
|
|
16
16
|
. "$ROOT_DIR/bin/lib/guardrail-mode.sh"
|
|
17
|
+
# shellcheck source=bin/lib/hook-read-policy.sh
|
|
18
|
+
. "$ROOT_DIR/bin/lib/hook-read-policy.sh"
|
|
17
19
|
load_guardrail_mode_for_hooks "${OPENCLAW_CONFIG_DIR:-$HOME/.claude}"
|
|
18
20
|
|
|
19
21
|
GUARDRAIL="$ROOT_DIR/bin/aport-guardrail-bash.sh"
|
|
@@ -49,7 +51,8 @@ set -e
|
|
|
49
51
|
if [ "$JQ_EXIT" -ne 0 ] || [ -z "$TOOL_NAME" ]; then
|
|
50
52
|
TOOL_NAME="unknown"
|
|
51
53
|
fi
|
|
52
|
-
|
|
54
|
+
# Strip permission-rule specifiers (e.g. Agent(Explore) -> Agent) before normalization.
|
|
55
|
+
TOOL_NAME_NORM="$(printf '%s' "$TOOL_NAME" | tr -d '[:space:]' | sed 's/^functions\.//' | sed 's/(.*$//' | tr '[:upper:]' '[:lower:]')"
|
|
53
56
|
set +e
|
|
54
57
|
TOOL_INPUT="$(echo "$INPUT" | jq -c '.tool_input // {}' 2> /dev/null)"
|
|
55
58
|
JQ_EXIT=$?
|
|
@@ -81,40 +84,45 @@ GUARDRAIL_TOOL=""
|
|
|
81
84
|
CONTEXT_JSON="{}"
|
|
82
85
|
|
|
83
86
|
case "$TOOL_NAME_NORM" in
|
|
84
|
-
bash | shell)
|
|
87
|
+
bash | shell | powershell | monitor)
|
|
85
88
|
GUARDRAIL_TOOL="bash"
|
|
86
|
-
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{command: (.command // "")}')"
|
|
89
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{command: (.command // .script // "")}')"
|
|
87
90
|
;;
|
|
88
|
-
read |
|
|
89
|
-
|
|
91
|
+
read | readfile | semanticsearch)
|
|
92
|
+
if ! aport_hook_try_read_evaluation "$TOOL_NAME_NORM" "$TOOL_INPUT"; then
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
;;
|
|
96
|
+
glob | ls | grep | lsp | todoread | toolsearch | askuserquestion | listmcpresourcestool | readmcpresourcetool | waitformcpservers)
|
|
97
|
+
# Search/list/read tools without a single file_path: allow without evaluator
|
|
90
98
|
exit 0
|
|
91
99
|
;;
|
|
92
|
-
taskget | tasklist | taskoutput | cronlist)
|
|
93
|
-
# Read-only task/cron queries: allow without evaluator
|
|
100
|
+
taskget | tasklist | taskoutput | cronlist | schedulewakeup | pushnotification)
|
|
101
|
+
# Read-only task/cron queries and notifications: allow without evaluator
|
|
94
102
|
exit 0
|
|
95
103
|
;;
|
|
96
104
|
enterplanmode | exitplanmode)
|
|
97
105
|
# Internal state transitions: allow without evaluator
|
|
98
106
|
exit 0
|
|
99
107
|
;;
|
|
100
|
-
write | edit | multiedit | notebookedit | todowrite | delete | strreplace | editnotebook)
|
|
108
|
+
write | edit | multiedit | notebookedit | todowrite | delete | strreplace | editnotebook | shareonboardingguide)
|
|
101
109
|
GUARDRAIL_TOOL="write"
|
|
102
110
|
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{file_path: (.file_path // .path // "")}')"
|
|
103
111
|
;;
|
|
104
112
|
websearch | webfetch)
|
|
105
|
-
GUARDRAIL_TOOL="
|
|
106
|
-
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // .query // "")}')"
|
|
113
|
+
GUARDRAIL_TOOL="websearch"
|
|
114
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // ""), query: (.query // "")}')"
|
|
107
115
|
;;
|
|
108
116
|
browser)
|
|
109
117
|
GUARDRAIL_TOOL="browser"
|
|
110
118
|
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // "")}')"
|
|
111
119
|
;;
|
|
112
|
-
task | taskcreate | taskupdate | taskstop |
|
|
120
|
+
agent | task | taskcreate | taskupdate | taskstop | skill | enterworktree | exitworktree | subagent | subagentstart | sendmessage | teamcreate | teamdelete | remotetrigger)
|
|
113
121
|
GUARDRAIL_TOOL="session.create"
|
|
114
|
-
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .prompt // "")}')"
|
|
122
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .prompt // .task // .message // ""), subagent_type: (.subagent_type // .agent_type // "")}')"
|
|
115
123
|
;;
|
|
116
124
|
croncreate | crondelete)
|
|
117
|
-
GUARDRAIL_TOOL="
|
|
125
|
+
GUARDRAIL_TOOL="session.create"
|
|
118
126
|
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .schedule // "")}')"
|
|
119
127
|
;;
|
|
120
128
|
mcp__* | mcp:* | callmcptool)
|
|
@@ -134,6 +142,14 @@ if [ -n "$HOOK_DECISION_FILE" ]; then
|
|
|
134
142
|
export OPENCLAW_DECISION_FILE="$HOOK_DECISION_FILE"
|
|
135
143
|
fi
|
|
136
144
|
|
|
145
|
+
# Read tools: send only file_path to the evaluator (Claude may attach large file bodies in tool_input).
|
|
146
|
+
if [ "$GUARDRAIL_TOOL" = "read" ]; then
|
|
147
|
+
CONTEXT_JSON="$(printf '%s' "$CONTEXT_JSON" | jq -c '{file_path: (.file_path // .path // "")}' 2> /dev/null || echo '{"file_path":""}')"
|
|
148
|
+
if [ -z "$(printf '%s' "$CONTEXT_JSON" | jq -r '.file_path // ""' 2> /dev/null)" ]; then
|
|
149
|
+
exit 0
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
|
|
137
153
|
# Call core evaluator (guardrail expects tool name, not policy ID)
|
|
138
154
|
set +e
|
|
139
155
|
"$GUARDRAIL" "$GUARDRAIL_TOOL" "$CONTEXT_JSON" 2> /dev/null
|
package/bin/aport-cursor-hook.sh
CHANGED
|
@@ -18,6 +18,8 @@ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
18
18
|
. "$ROOT_DIR/bin/aport-resolve-paths.sh"
|
|
19
19
|
# shellcheck source=bin/lib/guardrail-mode.sh
|
|
20
20
|
. "$ROOT_DIR/bin/lib/guardrail-mode.sh"
|
|
21
|
+
# shellcheck source=bin/lib/hook-read-policy.sh
|
|
22
|
+
. "$ROOT_DIR/bin/lib/hook-read-policy.sh"
|
|
21
23
|
load_guardrail_mode_for_hooks "${OPENCLAW_CONFIG_DIR:-$HOME/.cursor}"
|
|
22
24
|
|
|
23
25
|
GUARDRAIL="$ROOT_DIR/bin/aport-guardrail-bash.sh"
|
|
@@ -82,8 +84,10 @@ HOOK_EVENT="$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2> /dev/null)"
|
|
|
82
84
|
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // ""' 2> /dev/null)"
|
|
83
85
|
|
|
84
86
|
if [ "$HOOK_EVENT" = "beforeReadFile" ] || { [ -z "$HOOK_EVENT" ] && [ -z "$TOOL_NAME" ] && echo "$INPUT" | jq -e '.file_path and .content' &> /dev/null; }; then
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
FILE_PATH="$(echo "$INPUT" | jq -r '.file_path // ""' 2> /dev/null || true)"
|
|
88
|
+
if ! aport_hook_try_read_evaluation_from_file_path "$FILE_PATH"; then
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
87
91
|
|
|
88
92
|
elif [ "$HOOK_EVENT" = "subagentStart" ] || { [ -z "$HOOK_EVENT" ] && echo "$INPUT" | jq -e '.subagent_id' &> /dev/null; }; then
|
|
89
93
|
# subagentStart: sub-agent spawning
|
|
@@ -96,36 +100,48 @@ elif [ "$HOOK_EVENT" = "beforeMCPExecution" ] || { [ -n "$TOOL_NAME" ] && echo "
|
|
|
96
100
|
CONTEXT_JSON="$(safe_jq "$INPUT" '{tool_name: (.tool_name // ""), tool_input: (.tool_input // {})}')"
|
|
97
101
|
|
|
98
102
|
elif [ -n "$TOOL_NAME" ]; then
|
|
99
|
-
# preToolUse:
|
|
100
|
-
#
|
|
101
|
-
TOOL_NORM="$(
|
|
103
|
+
# preToolUse: Shell, Read, Write, Grep, Delete, Task, WebSearch, Agent, MCP:*, etc.
|
|
104
|
+
# See docs/FRAMEWORK_TOOL_MAPPING_AUDIT.md and https://cursor.com/docs/agent/hooks
|
|
105
|
+
TOOL_NORM="$(printf '%s' "$TOOL_NAME" | tr -d '[:space:]' | sed 's/^functions\.//' | sed 's/(.*$//' | tr '[:upper:]' '[:lower:]')"
|
|
102
106
|
case "$TOOL_NORM" in
|
|
103
|
-
shell)
|
|
107
|
+
shell | bash)
|
|
104
108
|
GUARDRAIL_TOOL="bash"
|
|
105
109
|
CONTEXT_JSON="$(safe_jq "$INPUT" '{command: (.tool_input.command // "")}')"
|
|
106
110
|
;;
|
|
107
|
-
read |
|
|
108
|
-
|
|
111
|
+
read | readfile | semanticsearch)
|
|
112
|
+
TOOL_INPUT="$(safe_jq "$INPUT" '.tool_input // {}')"
|
|
113
|
+
if ! aport_hook_try_read_evaluation "$TOOL_NORM" "$TOOL_INPUT"; then
|
|
114
|
+
exit 0
|
|
115
|
+
fi
|
|
116
|
+
;;
|
|
117
|
+
grep | glob | ls | lsp | listmcpresourcestool | readmcpresourcetool | toolsearch | waitformcpservers | taskget | tasklist | taskoutput | cronlist)
|
|
109
118
|
exit 0
|
|
110
119
|
;;
|
|
111
|
-
write | strreplace | editnotebook)
|
|
120
|
+
write | strreplace | edit | multiedit | editnotebook | applypatch | notebookedit | delete)
|
|
112
121
|
GUARDRAIL_TOOL="write"
|
|
113
122
|
CONTEXT_JSON="$(safe_jq "$INPUT" '{file_path: (.tool_input.file_path // .tool_input.path // "")}')"
|
|
114
123
|
;;
|
|
115
|
-
|
|
116
|
-
GUARDRAIL_TOOL="
|
|
117
|
-
CONTEXT_JSON="$(safe_jq "$INPUT" '{
|
|
124
|
+
websearch | webfetch)
|
|
125
|
+
GUARDRAIL_TOOL="websearch"
|
|
126
|
+
CONTEXT_JSON="$(safe_jq "$INPUT" '{url: (.tool_input.url // ""), query: (.tool_input.query // "")}')"
|
|
127
|
+
;;
|
|
128
|
+
browser)
|
|
129
|
+
GUARDRAIL_TOOL="browser"
|
|
130
|
+
CONTEXT_JSON="$(safe_jq "$INPUT" '{url: (.tool_input.url // "")}')"
|
|
118
131
|
;;
|
|
119
|
-
task)
|
|
132
|
+
task | agent | taskcreate | taskupdate | taskstop | skill | subagent | subagentstart | sendmessage | teamcreate | teamdelete)
|
|
120
133
|
GUARDRAIL_TOOL="session.create"
|
|
121
134
|
CONTEXT_JSON="$(safe_jq "$INPUT" '{description: (.tool_input.description // .tool_input.prompt // "")}')"
|
|
122
135
|
;;
|
|
123
|
-
|
|
136
|
+
croncreate | crondelete)
|
|
137
|
+
GUARDRAIL_TOOL="session.create"
|
|
138
|
+
CONTEXT_JSON="$(safe_jq "$INPUT" '{description: (.tool_input.description // .tool_input.schedule // "")}')"
|
|
139
|
+
;;
|
|
140
|
+
mcp__* | mcp:* | callmcptool)
|
|
124
141
|
GUARDRAIL_TOOL="mcp.tool"
|
|
125
142
|
CONTEXT_JSON="$(safe_jq "$INPUT" '{tool_name: (.tool_name // ""), tool_input: (.tool_input // {})}')"
|
|
126
143
|
;;
|
|
127
144
|
*)
|
|
128
|
-
# Unknown preToolUse tool: fail-closed
|
|
129
145
|
deny "🛡️ APort: unknown tool '$TOOL_NAME' — fail-closed policy"
|
|
130
146
|
;;
|
|
131
147
|
esac
|
|
@@ -154,6 +170,15 @@ if [ -n "$HOOK_DECISION_FILE" ]; then
|
|
|
154
170
|
export OPENCLAW_DECISION_FILE="$HOOK_DECISION_FILE"
|
|
155
171
|
fi
|
|
156
172
|
|
|
173
|
+
# Read tools: send only file_path to the evaluator (Cursor may attach large file bodies in tool_input).
|
|
174
|
+
if [ "$GUARDRAIL_TOOL" = "read" ]; then
|
|
175
|
+
CONTEXT_JSON="$(printf '%s' "$CONTEXT_JSON" | jq -c '{file_path: (.file_path // .path // "")}' 2> /dev/null || echo '{"file_path":""}')"
|
|
176
|
+
if [ -z "$(printf '%s' "$CONTEXT_JSON" | jq -r '.file_path // ""' 2> /dev/null)" ]; then
|
|
177
|
+
echo '{"permission":"allow","allowed":true}'
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
180
|
+
fi
|
|
181
|
+
|
|
157
182
|
# Call core evaluator
|
|
158
183
|
set +e
|
|
159
184
|
"$GUARDRAIL" "$GUARDRAIL_TOOL" "$CONTEXT_JSON" 2> /dev/null
|
|
@@ -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
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
LIB="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")/lib" && pwd)"
|
|
6
|
+
# shellcheck source=./lib/common.sh
|
|
7
|
+
source "$LIB/common.sh"
|
|
8
|
+
# shellcheck source=./lib/config.sh
|
|
9
|
+
source "$LIB/config.sh"
|
|
10
|
+
|
|
11
|
+
framework="${1:-}"
|
|
12
|
+
shift || true
|
|
13
|
+
|
|
14
|
+
if [[ -z "$framework" ]]; then
|
|
15
|
+
log_error "Usage: agent-guardrails reset <framework> [--yes]"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
yes_mode="${APORT_NONINTERACTIVE:-${CI:-}}"
|
|
20
|
+
while [[ $# -gt 0 ]]; do
|
|
21
|
+
case "$1" in
|
|
22
|
+
--yes | -y)
|
|
23
|
+
yes_mode=1
|
|
24
|
+
;;
|
|
25
|
+
*)
|
|
26
|
+
log_error "Unknown reset option: $1"
|
|
27
|
+
exit 1
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
shift
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
framework="$(echo "$framework" | tr '[:upper:]' '[:lower:]')"
|
|
34
|
+
config_dir="$(get_config_dir "$framework")"
|
|
35
|
+
config_dir="${config_dir/#\~/$HOME}"
|
|
36
|
+
|
|
37
|
+
confirm_reset() {
|
|
38
|
+
if [[ -n "$yes_mode" ]]; then
|
|
39
|
+
return 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo ""
|
|
43
|
+
echo " Reset APort for $framework"
|
|
44
|
+
echo " ──────────────────────────"
|
|
45
|
+
echo " This removes APort-owned local config and hook/plugin wiring for this framework."
|
|
46
|
+
echo " Other framework settings are preserved when possible."
|
|
47
|
+
echo ""
|
|
48
|
+
|
|
49
|
+
local answer
|
|
50
|
+
read -r -p " Continue? [y/N]: " answer
|
|
51
|
+
case "$answer" in
|
|
52
|
+
y | Y | yes | YES) ;;
|
|
53
|
+
*)
|
|
54
|
+
echo " Reset cancelled."
|
|
55
|
+
exit 0
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
backup_file() {
|
|
61
|
+
local file="$1"
|
|
62
|
+
if [[ -f "$file" ]]; then
|
|
63
|
+
cp "$file" "${file}.bak"
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
remove_dir_if_exists() {
|
|
68
|
+
local dir="$1"
|
|
69
|
+
if [[ -d "$dir" ]]; then
|
|
70
|
+
rm -rf "$dir"
|
|
71
|
+
echo " ✅ Removed $dir"
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
remove_file_if_exists() {
|
|
76
|
+
local file="$1"
|
|
77
|
+
if [[ -f "$file" ]]; then
|
|
78
|
+
rm -f "$file"
|
|
79
|
+
echo " ✅ Removed $file"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cleanup_claude_settings() {
|
|
84
|
+
local settings_file="$1"
|
|
85
|
+
if [[ ! -f "$settings_file" ]]; then
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
if ! command -v jq &> /dev/null; then
|
|
89
|
+
log_warn "jq not found; leaving Claude settings intact at $settings_file"
|
|
90
|
+
return 0
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
local tmpfile
|
|
94
|
+
tmpfile="$(mktemp "${settings_file}.XXXXXX")"
|
|
95
|
+
if jq '
|
|
96
|
+
if (.hooks.PreToolUse? // null) == null then
|
|
97
|
+
.
|
|
98
|
+
else
|
|
99
|
+
.hooks.PreToolUse = (
|
|
100
|
+
(.hooks.PreToolUse // [])
|
|
101
|
+
| map(
|
|
102
|
+
if (.hooks? // null) == null then
|
|
103
|
+
.
|
|
104
|
+
else
|
|
105
|
+
.hooks = (
|
|
106
|
+
(.hooks // [])
|
|
107
|
+
| map(select((((.command // "") | test("aport-(cursor-hook|claude-code-hook)\\.sh$")) | not)))
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
)
|
|
111
|
+
| map(select(((.hooks? // []) | length) > 0))
|
|
112
|
+
)
|
|
113
|
+
| if ((.hooks.PreToolUse // []) | length) == 0 then del(.hooks.PreToolUse) else . end
|
|
114
|
+
| if ((.hooks // {}) | keys | length) == 0 then del(.hooks) else . end
|
|
115
|
+
end
|
|
116
|
+
' "$settings_file" > "$tmpfile"; then
|
|
117
|
+
backup_file "$settings_file"
|
|
118
|
+
mv "$tmpfile" "$settings_file"
|
|
119
|
+
echo " ✅ Removed APort Claude Code hook entries from $settings_file"
|
|
120
|
+
else
|
|
121
|
+
rm -f "$tmpfile"
|
|
122
|
+
log_warn "Failed to clean Claude settings at $settings_file"
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
cleanup_cursor_hooks() {
|
|
127
|
+
local hooks_file="$1"
|
|
128
|
+
if [[ ! -f "$hooks_file" ]]; then
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
if ! command -v jq &> /dev/null; then
|
|
132
|
+
log_warn "jq not found; leaving Cursor hooks intact at $hooks_file"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
local tmpfile
|
|
137
|
+
tmpfile="$(mktemp "${hooks_file}.XXXXXX")"
|
|
138
|
+
if jq '
|
|
139
|
+
def strip_aport_hooks:
|
|
140
|
+
(. // []) | map(select((((.command // "") | test("aport-(cursor-hook|claude-code-hook)\\.sh$")) | not)));
|
|
141
|
+
.hooks.beforeShellExecution = ((.hooks.beforeShellExecution // []) | strip_aport_hooks) |
|
|
142
|
+
.hooks.preToolUse = ((.hooks.preToolUse // []) | strip_aport_hooks) |
|
|
143
|
+
.hooks.beforeMCPExecution = ((.hooks.beforeMCPExecution // []) | strip_aport_hooks) |
|
|
144
|
+
.hooks.subagentStart = ((.hooks.subagentStart // []) | strip_aport_hooks) |
|
|
145
|
+
if ((.hooks.beforeShellExecution // []) | length) == 0 then del(.hooks.beforeShellExecution) else . end |
|
|
146
|
+
if ((.hooks.preToolUse // []) | length) == 0 then del(.hooks.preToolUse) else . end |
|
|
147
|
+
if ((.hooks.beforeMCPExecution // []) | length) == 0 then del(.hooks.beforeMCPExecution) else . end |
|
|
148
|
+
if ((.hooks.subagentStart // []) | length) == 0 then del(.hooks.subagentStart) else . end |
|
|
149
|
+
if ((.hooks // {}) | keys | length) == 0 then del(.hooks) else . end
|
|
150
|
+
' "$hooks_file" > "$tmpfile"; then
|
|
151
|
+
backup_file "$hooks_file"
|
|
152
|
+
mv "$tmpfile" "$hooks_file"
|
|
153
|
+
echo " ✅ Removed APort Cursor hook entries from $hooks_file"
|
|
154
|
+
else
|
|
155
|
+
rm -f "$tmpfile"
|
|
156
|
+
log_warn "Failed to clean Cursor hooks at $hooks_file"
|
|
157
|
+
fi
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
cleanup_openclaw_json() {
|
|
161
|
+
local openclaw_json="$1"
|
|
162
|
+
if [[ ! -f "$openclaw_json" ]]; then
|
|
163
|
+
return 0
|
|
164
|
+
fi
|
|
165
|
+
if ! command -v jq &> /dev/null; then
|
|
166
|
+
log_warn "jq not found; leaving OpenClaw JSON config intact at $openclaw_json"
|
|
167
|
+
return 0
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
local tmpfile
|
|
171
|
+
tmpfile="$(mktemp "${openclaw_json}.XXXXXX")"
|
|
172
|
+
if jq '
|
|
173
|
+
.plugins = (.plugins // {}) |
|
|
174
|
+
.plugins.entries = ((.plugins.entries // {}) | del(.["openclaw-aport"])) |
|
|
175
|
+
.plugins.installs = ((.plugins.installs // {}) | del(.["openclaw-aport"])) |
|
|
176
|
+
.plugins.load = (.plugins.load // {}) |
|
|
177
|
+
.plugins.load.paths = ((.plugins.load.paths // []) | map(select((type == "string" and contains("/openclaw-aport")) | not))) |
|
|
178
|
+
if ((.plugins.entries // {}) | keys | length) == 0 then del(.plugins.entries) else . end |
|
|
179
|
+
if ((.plugins.installs // {}) | keys | length) == 0 then del(.plugins.installs) else . end |
|
|
180
|
+
if ((.plugins.load.paths // []) | length) == 0 then del(.plugins.load.paths) else . end |
|
|
181
|
+
if ((.plugins.load // {}) | keys | length) == 0 then del(.plugins.load) else . end |
|
|
182
|
+
if ((.plugins // {}) | keys | length) == 0 then del(.plugins) else . end
|
|
183
|
+
' "$openclaw_json" > "$tmpfile"; then
|
|
184
|
+
backup_file "$openclaw_json"
|
|
185
|
+
mv "$tmpfile" "$openclaw_json"
|
|
186
|
+
echo " ✅ Removed APort OpenClaw entries from $openclaw_json"
|
|
187
|
+
else
|
|
188
|
+
rm -f "$tmpfile"
|
|
189
|
+
log_warn "Failed to clean OpenClaw JSON config at $openclaw_json"
|
|
190
|
+
fi
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
cleanup_python_framework() {
|
|
194
|
+
local config_dir="$1"
|
|
195
|
+
remove_dir_if_exists "$config_dir/aport"
|
|
196
|
+
remove_file_if_exists "$config_dir/config.yaml"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
cleanup_n8n() {
|
|
200
|
+
local config_dir="$1"
|
|
201
|
+
remove_dir_if_exists "$config_dir/aport"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
cleanup_claude() {
|
|
205
|
+
local settings_file="$config_dir/settings.json"
|
|
206
|
+
remove_dir_if_exists "$config_dir/aport"
|
|
207
|
+
cleanup_claude_settings "$settings_file"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
cleanup_cursor() {
|
|
211
|
+
local hooks_file="$config_dir/hooks.json"
|
|
212
|
+
remove_dir_if_exists "$config_dir/aport"
|
|
213
|
+
cleanup_cursor_hooks "$hooks_file"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
cleanup_openclaw() {
|
|
217
|
+
local openclaw_json="$config_dir/openclaw.json"
|
|
218
|
+
remove_dir_if_exists "$config_dir/aport"
|
|
219
|
+
remove_dir_if_exists "$config_dir/extensions/openclaw-aport"
|
|
220
|
+
remove_dir_if_exists "$config_dir/skills/aport-guardrail"
|
|
221
|
+
remove_file_if_exists "$config_dir/.aport-repo"
|
|
222
|
+
remove_file_if_exists "$config_dir/.skills/aport-guardrail.sh"
|
|
223
|
+
remove_file_if_exists "$config_dir/.skills/aport-guardrail-bash.sh"
|
|
224
|
+
remove_file_if_exists "$config_dir/.skills/aport-guardrail-api.sh"
|
|
225
|
+
remove_file_if_exists "$config_dir/.skills/aport-guardrail-v2.sh"
|
|
226
|
+
remove_file_if_exists "$config_dir/.skills/aport-create-passport.sh"
|
|
227
|
+
remove_file_if_exists "$config_dir/.skills/aport-status.sh"
|
|
228
|
+
cleanup_openclaw_json "$openclaw_json"
|
|
229
|
+
if [[ -f "$config_dir/config.yaml" ]]; then
|
|
230
|
+
log_warn "OpenClaw config.yaml may still contain APort plugin config. Review $config_dir/config.yaml if you need a completely pristine OpenClaw config."
|
|
231
|
+
fi
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
confirm_reset
|
|
235
|
+
|
|
236
|
+
echo "[aport] Resetting framework: $framework" >&2
|
|
237
|
+
|
|
238
|
+
case "$framework" in
|
|
239
|
+
claude-code)
|
|
240
|
+
cleanup_claude
|
|
241
|
+
;;
|
|
242
|
+
cursor)
|
|
243
|
+
cleanup_cursor
|
|
244
|
+
;;
|
|
245
|
+
openclaw)
|
|
246
|
+
cleanup_openclaw
|
|
247
|
+
;;
|
|
248
|
+
langchain | crewai | deerflow)
|
|
249
|
+
cleanup_python_framework "$config_dir"
|
|
250
|
+
;;
|
|
251
|
+
n8n)
|
|
252
|
+
cleanup_n8n "$config_dir"
|
|
253
|
+
;;
|
|
254
|
+
*)
|
|
255
|
+
log_error "Unsupported framework reset: $framework"
|
|
256
|
+
exit 1
|
|
257
|
+
;;
|
|
258
|
+
esac
|
|
259
|
+
|
|
260
|
+
echo ""
|
|
261
|
+
echo " Reset complete for $framework."
|
|
262
|
+
echo " Re-run the setup command when you want a fresh install."
|
|
263
|
+
echo ""
|