@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.
Files changed (35) hide show
  1. package/README.md +30 -6
  2. package/bin/aport-claude-code-hook.sh +38 -16
  3. package/bin/aport-create-passport.sh +3 -2
  4. package/bin/aport-cursor-hook.sh +49 -18
  5. package/bin/aport-guardrail-api.sh +2 -0
  6. package/bin/aport-guardrail-bash.sh +15 -0
  7. package/bin/aport-resolve-paths.sh +67 -40
  8. package/bin/frameworks/claude-code.sh +11 -1
  9. package/bin/frameworks/cursor.sh +6 -1
  10. package/bin/frameworks/generic.sh +9 -0
  11. package/bin/lib/framework-hook-paths.sh +96 -0
  12. package/bin/lib/guardrail-mode.sh +44 -3
  13. package/bin/lib/hook-read-policy.sh +61 -0
  14. package/bin/lib/quick-hosted.sh +164 -0
  15. package/bin/lib/tool-mapping.sh +5 -1
  16. package/bin/openclaw +76 -45
  17. package/docs/FRAMEWORK_TOOL_MAPPING_AUDIT.md +158 -0
  18. package/docs/HOSTED_PASSPORT_SETUP.md +29 -4
  19. package/docs/QUICKSTART.md +9 -3
  20. package/docs/README.md +3 -3
  21. package/docs/RELEASE.md +1 -1
  22. package/docs/TOOL_POLICY_MAPPING.md +6 -1
  23. package/docs/VERIFICATION_METHODS.md +1 -1
  24. package/docs/frameworks/claude-code.md +34 -7
  25. package/docs/frameworks/cursor.md +4 -4
  26. package/docs/frameworks/deerflow.md +12 -8
  27. package/extensions/openclaw-aport/CHANGELOG.md +4 -0
  28. package/extensions/openclaw-aport/local-evaluator.js +17 -0
  29. package/extensions/openclaw-aport/openclaw.plugin.json +1 -1
  30. package/extensions/openclaw-aport/package-lock.json +2 -2
  31. package/extensions/openclaw-aport/package.json +1 -1
  32. package/extensions/openclaw-aport/tool-mapping.js +33 -4
  33. package/package.json +1 -1
  34. package/python/aport_guardrails/core/tool-pack-mapping.json +111 -1
  35. 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 30 seconds
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
- - OpenClaw direct: `npx @aporthq/aport-agent-guardrails openclaw`
57
- - Hosted passport: `npx @aporthq/aport-agent-guardrails openclaw <agent_id>`
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 the **passport wizard** and writes config for your framework. 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).
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 the wizard: `npx @aporthq/aport-agent-guardrails openclaw <agent_id>`. See [Hosted passport setup](docs/HOSTED_PASSPORT_SETUP.md).
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) | 5-minute OpenClaw setup |
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
- load_guardrail_mode_for_hooks "${OPENCLAW_CONFIG_DIR:-$HOME/.claude}"
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
- TOOL_NAME_NORM="$(printf '%s' "$TOOL_NAME" | tr -d '[:space:]' | sed 's/^functions\.//' | tr '[:upper:]' '[:lower:]')"
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
- read | glob | ls | grep | todoread | toolsearch | askuserquestion | readfile | semanticsearch)
89
- # Read-family + user-interaction tools: allow without calling evaluator
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="webfetch"
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 | agent | skill | enterworktree | subagent | subagentstart)
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="cron"
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/#\~/$HOME}"
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
@@ -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
- load_guardrail_mode_for_hooks "${OPENCLAW_CONFIG_DIR:-$HOME/.cursor}"
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
- # beforeReadFile: file reads allow without evaluator (same as Claude Code)
86
- exit 0
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: Cursor tool names — Shell, Read, Write, Grep, Delete, Task, MCP:<name>
100
- # Normalize: trim whitespace, lowercase (patterns must match TOOL_NORM)
101
- TOOL_NORM="$(echo "$TOOL_NAME" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')"
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 | grep | glob | semanticsearch)
108
- # Read-family: allow without calling evaluator (matches Claude Code behavior)
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
- delete)
116
- GUARDRAIL_TOOL="write"
117
- CONTEXT_JSON="$(safe_jq "$INPUT" '{file_path: (.tool_input.file_path // .tool_input.path // "")}')"
129
+ websearch | webfetch)
130
+ GUARDRAIL_TOOL="websearch"
131
+ CONTEXT_JSON="$(safe_jq "$INPUT" '{url: (.tool_input.url // ""), query: (.tool_input.query // "")}')"
118
132
  ;;
119
- task)
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
- mcp:*)
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: OPENCLAW_PASSPORT_FILE, OPENCLAW_DECISION_FILE, OPENCLAW_AUDIT_LOG
9
- # and PASSPORT_FILE, DECISION_FILE, AUDIT_LOG. Passport status (active|suspended|revoked) is source of truth for suspend; no separate file.
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 — OPENCLAW_PASSPORT_FILE always wins.
29
- if [ -z "${OPENCLAW_PASSPORT_FILE:-}" ]; then
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
- if [ -z "${OPENCLAW_DECISION_FILE:-}" ]; then
44
- export OPENCLAW_DECISION_FILE="${data_dir}/decision.json"
45
- fi
46
- export OPENCLAW_AUDIT_LOG="${data_dir}/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="${OPENCLAW_DECISION_FILE}"
49
- AUDIT_LOG="${data_dir}/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 use it (plugin or wrapper)
57
- if [ -n "${OPENCLAW_PASSPORT_FILE:-}" ] && [ -f "$OPENCLAW_PASSPORT_FILE" ]; then
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 "$OPENCLAW_PASSPORT_FILE"; then
61
- echo "[aport] WARN: OPENCLAW_PASSPORT_FILE path failed validation: $OPENCLAW_PASSPORT_FILE" >&2
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 "$OPENCLAW_PASSPORT_FILE")"
65
- passport_path="$OPENCLAW_PASSPORT_FILE"
66
- # 2) Explicit path set but file missing legacy: try parent dir (e.g. .../openclaw/passport.json)
67
- elif [ -n "${OPENCLAW_PASSPORT_FILE:-}" ]; then
68
- config_dir="$(cd "$(dirname "$OPENCLAW_PASSPORT_FILE")/.." 2> /dev/null && pwd)"
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="$OPENCLAW_PASSPORT_FILE"
74
- data_dir="$(dirname "$OPENCLAW_PASSPORT_FILE")"
88
+ passport_path="$explicit_passport"
89
+ data_dir="$(dirname "$explicit_passport")"
75
90
  fi
76
- # 3) No env → probe framework-specific default paths (where each framework stores data), then OpenClaw legacy
91
+ # 3) Probe framework paths, or honor APORT_CONFIG_DIR / OPENCLAW_CONFIG_DIR set by framework hooks.
77
92
  else
78
- config_dir=""
79
- for candidate in "$HOME/.claude" "$HOME/.cursor" "$HOME/.openclaw" "$HOME/.aport/langchain" "$HOME/.aport/crewai" "$HOME/.aport/deerflow" "$HOME/.n8n"; do
80
- if [ -f "${candidate}/aport/passport.json" ]; then
81
- config_dir="$candidate"
82
- break
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
- # Preserve explicitly set decision path (e.g. tests set OPENCLAW_DECISION_FILE); otherwise use data_dir
103
- if [ -n "${OPENCLAW_DECISION_FILE:-}" ]; then
104
- export OPENCLAW_DECISION_FILE="$OPENCLAW_DECISION_FILE"
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 OPENCLAW_DECISION_FILE="${data_dir}/decision.json"
133
+ export APORT_AUDIT_LOG="${data_dir}/audit.log"
107
134
  fi
108
- export OPENCLAW_AUDIT_LOG="${data_dir}/audit.log"
135
+ export OPENCLAW_AUDIT_LOG="$APORT_AUDIT_LOG"
109
136
 
110
- PASSPORT_FILE="$OPENCLAW_PASSPORT_FILE"
111
- DECISION_FILE="$OPENCLAW_DECISION_FILE"
112
- AUDIT_LOG="$OPENCLAW_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"
@@ -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 " Same script works for VS Code + Copilot. For Claude Code use the dedicated integration: docs/frameworks/claude-code.md"
110
+ echo " For other frameworks like Claude Code, use the dedicated integration: docs/frameworks"
106
111
  echo ""
107
112
  }
108
113