@aporthq/aport-agent-guardrails 1.0.11 → 1.0.13
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 +4 -3
- package/bin/agent-guardrails +13 -6
- package/bin/aport-claude-code-hook.sh +147 -0
- package/bin/aport-create-passport.sh +5 -0
- package/bin/aport-guardrail-bash.sh +7 -3
- package/bin/aport-resolve-paths.sh +17 -1
- package/bin/aport-status.sh +6 -0
- package/bin/frameworks/claude-code.sh +114 -0
- package/bin/frameworks/crewai.sh +3 -0
- package/bin/frameworks/cursor.sh +11 -3
- package/bin/frameworks/langchain.sh +3 -0
- package/bin/frameworks/n8n.sh +3 -0
- package/bin/lib/allowlist.sh +6 -4
- package/bin/lib/config.sh +1 -0
- package/bin/lib/detect.sh +6 -1
- package/bin/lib/validation.sh +27 -7
- package/bin/openclaw +10 -4
- package/docs/OPENCLAW_TOOLS_AND_POLICIES.md +18 -0
- package/docs/RELEASE.md +1 -1
- package/docs/SECURITY_MODEL.md +27 -3
- package/docs/VERIFICATION_METHODS.md +1 -0
- package/docs/frameworks/claude-code.md +109 -0
- package/docs/frameworks/crewai.md +5 -0
- package/docs/frameworks/cursor.md +16 -7
- package/docs/frameworks/langchain.md +5 -0
- package/docs/launch/PRD-claude-code-guardrail.md +753 -0
- package/extensions/openclaw-aport/README.md +1 -1
- package/extensions/openclaw-aport/index.ts +60 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ Your agent should **only do what you explicitly allow**. APort runs in the hook
|
|
|
60
60
|
|
|
61
61
|
| Policy | What it guards |
|
|
62
62
|
|--------|----------------|
|
|
63
|
-
| **system.command.execute.v1** | Shell commands — allowlist,
|
|
63
|
+
| **system.command.execute.v1** | Shell commands — allowlist, 50+ blocked patterns (`rm -rf`, `sudo`, `nc`, `find -exec rm`, injection); passport `allowed_paths` override for path rules |
|
|
64
64
|
| **mcp.tool.execute.v1** | MCP tool calls — server allowlist, rate limits |
|
|
65
65
|
| **messaging.message.send.v1** | Message sends — rate caps, capability checks |
|
|
66
66
|
| **agent.session.create.v1** / **agent.tool.register.v1** | Sessions and tool registration |
|
|
@@ -86,12 +86,13 @@ The **kill switch suspends the agent**: once active, every tool call is denied u
|
|
|
86
86
|
|
|
87
87
|
**Two ways to use APort:** (1) **Guardrails (CLI/setup)** — run the installer to create your passport and config; (2) **Core (library)** — use the evaluator or framework callback in your app so each tool call is verified. Each framework doc ([LangChain](docs/frameworks/langchain.md), [CrewAI](docs/frameworks/crewai.md), [Cursor](docs/frameworks/cursor.md), [OpenClaw](docs/frameworks/openclaw.md)) describes both and how to use them for that framework (Python and Node where applicable).
|
|
88
88
|
|
|
89
|
-
**Production-ready today:** OpenClaw (plugin + full installer), Cursor (hooks), **Python** LangChain/CrewAI (`pip install aport-agent-guardrails-langchain` / `aport-agent-guardrails-crewai`), and **Node** (CLI + `@aporthq/aport-agent-guardrails-core`, `-langchain`, `-crewai`, `-cursor` published via CI). See [Deployment readiness](docs/DEPLOYMENT_READINESS.md).
|
|
89
|
+
**Production-ready today:** OpenClaw (plugin + full installer), Cursor (hooks), **Claude Code** (PreToolUse hook), **Python** LangChain/CrewAI (`pip install aport-agent-guardrails-langchain` / `aport-agent-guardrails-crewai`), and **Node** (CLI + `@aporthq/aport-agent-guardrails-core`, `-langchain`, `-crewai`, `-cursor`, `-claude-code` published via CI). See [Deployment readiness](docs/DEPLOYMENT_READINESS.md).
|
|
90
90
|
|
|
91
91
|
| Framework | Doc | Integration | Install |
|
|
92
92
|
|-----------|-----|--------------|--------|
|
|
93
93
|
| **OpenClaw** | [docs/frameworks/openclaw.md](docs/frameworks/openclaw.md) | `before_tool_call` plugin | `npx @aporthq/aport-agent-guardrails openclaw` |
|
|
94
94
|
| **Cursor** | [docs/frameworks/cursor.md](docs/frameworks/cursor.md) | `beforeShellExecution` / `preToolUse` hooks → writes `~/.cursor/hooks.json`. **Runtime enforcement is the bash hook;** the Node package `@aporthq/aport-agent-guardrails-cursor` is a helper only (Evaluator, `getHookPath()`). | `npx @aporthq/aport-agent-guardrails cursor` |
|
|
95
|
+
| **Claude Code** | [docs/frameworks/claude-code.md](docs/frameworks/claude-code.md) | PreToolUse hook → writes `~/.claude/settings.json` (Claude Code format; not Cursor). | `npx @aporthq/aport-agent-guardrails claude-code` |
|
|
95
96
|
| **LangChain / LangGraph** | [docs/frameworks/langchain.md](docs/frameworks/langchain.md) | **Python:** `APortCallback` (`on_tool_start`) | `npx @aporthq/aport-agent-guardrails langchain` then `pip install aport-agent-guardrails-langchain` + `aport-langchain setup` |
|
|
96
97
|
| **CrewAI** | [docs/frameworks/crewai.md](docs/frameworks/crewai.md) | **Python:** `@before_tool_call` hook, `register_aport_guardrail` | `npx @aporthq/aport-agent-guardrails crewai` then `pip install aport-agent-guardrails-crewai` + `aport-crewai setup` |
|
|
97
98
|
| **n8n** | [docs/frameworks/n8n.md](docs/frameworks/n8n.md) | *Coming soon* — custom node and runtime in progress | — |
|
|
@@ -335,7 +336,7 @@ graph TB
|
|
|
335
336
|
**✅ Pre-action authorization (agent misbehavior):**
|
|
336
337
|
- **Prompt injection** - Hook-based enforcement; agent cannot bypass via prompts
|
|
337
338
|
- **Malicious skills** - Third-party OpenClaw skills validated before execution
|
|
338
|
-
- **Unauthorized commands** - Allowlist +
|
|
339
|
+
- **Unauthorized commands** - Allowlist + 50+ blocked patterns (rm -rf, sudo, nc, find -exec rm, etc.)
|
|
339
340
|
- **Data exfiltration** - File access, messaging, web requests controlled by policy
|
|
340
341
|
- **Resource limits** - Rate limits, size caps, transaction amounts enforced
|
|
341
342
|
|
package/bin/agent-guardrails
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# agent-guardrails openclaw ap_xxx # openclaw with agent_id (pass-through)
|
|
7
7
|
# agent-guardrails --framework=langchain
|
|
8
8
|
# agent-guardrails -f crewai
|
|
9
|
-
# Supported: openclaw | langchain | crewai | cursor (n8n coming soon)
|
|
9
|
+
# Supported: openclaw | langchain | crewai | cursor | claude-code (n8n coming soon)
|
|
10
10
|
#
|
|
11
11
|
# Backward compatibility: All arguments after the framework are passed through to
|
|
12
12
|
# the framework script. For OpenClaw, bin/openclaw receives them (e.g. agent_id).
|
|
@@ -72,14 +72,14 @@ if [[ -z "$framework" ]]; then
|
|
|
72
72
|
noninteractive="${APORT_NONINTERACTIVE:-${CI:-}}"
|
|
73
73
|
if [[ -n "$noninteractive" ]]; then
|
|
74
74
|
echo "[aport] ERROR: Multiple frameworks detected: $detected_list" >&2
|
|
75
|
-
echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor" >&2
|
|
75
|
+
echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor | claude-code" >&2
|
|
76
76
|
exit 1
|
|
77
77
|
fi
|
|
78
78
|
echo ""
|
|
79
79
|
echo " APort Agent Guardrails — Framework setup"
|
|
80
80
|
echo " ─────────────────────────────────────────"
|
|
81
81
|
echo " Multiple frameworks detected: $detected_list"
|
|
82
|
-
echo " Choose one: openclaw | langchain | crewai | cursor"
|
|
82
|
+
echo " Choose one: openclaw | langchain | crewai | cursor | claude-code"
|
|
83
83
|
echo " Example: npx @aporthq/agent-guardrails --framework=openclaw"
|
|
84
84
|
echo ""
|
|
85
85
|
read -p " Framework [${framework:-openclaw}]: " choice
|
|
@@ -95,14 +95,14 @@ if [[ -z "$framework" ]]; then
|
|
|
95
95
|
noninteractive="${APORT_NONINTERACTIVE:-${CI:-}}"
|
|
96
96
|
if [[ -n "$noninteractive" ]]; then
|
|
97
97
|
echo "[aport] ERROR: No framework detected in current directory." >&2
|
|
98
|
-
echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor" >&2
|
|
98
|
+
echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor | claude-code" >&2
|
|
99
99
|
exit 1
|
|
100
100
|
fi
|
|
101
101
|
echo ""
|
|
102
102
|
echo " APort Agent Guardrails — Framework setup"
|
|
103
103
|
echo " ─────────────────────────────────────────"
|
|
104
104
|
echo " No framework detected in current directory."
|
|
105
|
-
echo " Choose: openclaw | langchain | crewai | cursor"
|
|
105
|
+
echo " Choose: openclaw | langchain | crewai | cursor | claude-code"
|
|
106
106
|
echo " Example: npx @aporthq/agent-guardrails openclaw"
|
|
107
107
|
echo " npx @aporthq/agent-guardrails --framework=langchain"
|
|
108
108
|
echo ""
|
|
@@ -111,6 +111,13 @@ if [[ -z "$framework" ]]; then
|
|
|
111
111
|
fi
|
|
112
112
|
|
|
113
113
|
framework="$(echo "$framework" | tr '[:upper:]' '[:lower:]')"
|
|
114
|
+
|
|
115
|
+
# SECURITY: Validate framework name (alphanumeric + hyphen only, prevents path traversal)
|
|
116
|
+
if [[ ! "$framework" =~ ^[a-z0-9-]+$ ]]; then
|
|
117
|
+
echo "[aport] ERROR: Invalid framework name: $framework (alphanumeric and hyphens only)" >&2
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
114
121
|
script="$FRAMEWORKS_DIR/${framework}.sh"
|
|
115
122
|
|
|
116
123
|
# n8n: custom node not yet available; warn before running wizard-only script
|
|
@@ -129,5 +136,5 @@ if [[ "$framework" == "openclaw" ]]; then
|
|
|
129
136
|
fi
|
|
130
137
|
|
|
131
138
|
echo "[aport] ERROR: Unknown or unsupported framework: $framework" >&2
|
|
132
|
-
echo " Supported: openclaw, langchain, crewai, cursor (n8n coming soon)" >&2
|
|
139
|
+
echo " Supported: openclaw, langchain, crewai, cursor, claude-code (n8n coming soon)" >&2
|
|
133
140
|
exit 1
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# APort Claude Code hook: reads tool_name + tool_input from JSON stdin,
|
|
3
|
+
# maps to APort policy, calls guardrail, outputs hookSpecificOutput deny or exit 0.
|
|
4
|
+
# Exit 0 = allow, exit 2 = block. Other exits = hook error (Claude Code may fail-open).
|
|
5
|
+
# Output format: Claude Code official schema (hookSpecificOutput.permissionDecision), NOT Cursor format.
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
11
|
+
GUARDRAIL="$ROOT_DIR/bin/aport-guardrail-bash.sh"
|
|
12
|
+
|
|
13
|
+
# Path resolver: probes ~/.claude, ~/.cursor, ~/.openclaw, etc.
|
|
14
|
+
# shellcheck source=bin/aport-resolve-paths.sh
|
|
15
|
+
. "$ROOT_DIR/bin/aport-resolve-paths.sh"
|
|
16
|
+
|
|
17
|
+
# Read stdin
|
|
18
|
+
INPUT=""
|
|
19
|
+
if [ -t 0 ]; then
|
|
20
|
+
INPUT='{}'
|
|
21
|
+
else
|
|
22
|
+
INPUT="$(cat)"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# No input = allow (fail-open for bad input)
|
|
26
|
+
[ -z "$INPUT" ] && exit 0
|
|
27
|
+
|
|
28
|
+
# Parse tool_name and tool_input (requires jq)
|
|
29
|
+
if ! command -v jq &> /dev/null; then
|
|
30
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"🛡️ APort: jq is required"}}'
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Parse with error handling: jq failure must exit 2 (deny), never undefined exit codes
|
|
35
|
+
set +e
|
|
36
|
+
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // "unknown"' 2> /dev/null)"
|
|
37
|
+
JQ_EXIT=$?
|
|
38
|
+
set -e
|
|
39
|
+
if [ "$JQ_EXIT" -ne 0 ] || [ -z "$TOOL_NAME" ]; then
|
|
40
|
+
TOOL_NAME="unknown"
|
|
41
|
+
fi
|
|
42
|
+
set +e
|
|
43
|
+
TOOL_INPUT="$(echo "$INPUT" | jq -c '.tool_input // {}' 2> /dev/null)"
|
|
44
|
+
JQ_EXIT=$?
|
|
45
|
+
set -e
|
|
46
|
+
if [ "$JQ_EXIT" -ne 0 ] || [ -z "$TOOL_INPUT" ]; then
|
|
47
|
+
TOOL_INPUT='{}'
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Safe jq extraction: returns '{}' on any jq error instead of crashing under set -e
|
|
51
|
+
safe_jq() {
|
|
52
|
+
local input="$1" filter="$2"
|
|
53
|
+
local result
|
|
54
|
+
result="$(echo "$input" | jq -c "$filter" 2> /dev/null)" || result='{}'
|
|
55
|
+
[ -z "$result" ] && result='{}'
|
|
56
|
+
echo "$result"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Deny helper: outputs Claude Code hookSpecificOutput JSON and exits 2
|
|
60
|
+
deny() {
|
|
61
|
+
local reason="$1"
|
|
62
|
+
jq -n --arg reason "$reason" \
|
|
63
|
+
--arg event "PreToolUse" \
|
|
64
|
+
'{hookSpecificOutput:{hookEventName:$event,permissionDecision:"deny",permissionDecisionReason:$reason}}'
|
|
65
|
+
exit 2
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Tool name passed to guardrail (must match aport-guardrail-bash.sh case patterns)
|
|
69
|
+
GUARDRAIL_TOOL=""
|
|
70
|
+
CONTEXT_JSON="{}"
|
|
71
|
+
|
|
72
|
+
case "$TOOL_NAME" in
|
|
73
|
+
Bash)
|
|
74
|
+
GUARDRAIL_TOOL="bash"
|
|
75
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{command: (.command // "")}')"
|
|
76
|
+
;;
|
|
77
|
+
Read | Glob | LS | Grep | TodoRead | ToolSearch | AskUserQuestion)
|
|
78
|
+
# Read-family + user-interaction tools: allow without calling evaluator
|
|
79
|
+
exit 0
|
|
80
|
+
;;
|
|
81
|
+
TaskGet | TaskList | TaskOutput | CronList)
|
|
82
|
+
# Read-only task/cron queries: allow without evaluator
|
|
83
|
+
exit 0
|
|
84
|
+
;;
|
|
85
|
+
EnterPlanMode | ExitPlanMode)
|
|
86
|
+
# Internal state transitions: allow without evaluator
|
|
87
|
+
exit 0
|
|
88
|
+
;;
|
|
89
|
+
Write | Edit | MultiEdit | NotebookEdit | TodoWrite)
|
|
90
|
+
GUARDRAIL_TOOL="write"
|
|
91
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{file_path: (.file_path // .path // "")}')"
|
|
92
|
+
;;
|
|
93
|
+
WebSearch | WebFetch)
|
|
94
|
+
GUARDRAIL_TOOL="webfetch"
|
|
95
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // .query // "")}')"
|
|
96
|
+
;;
|
|
97
|
+
Browser)
|
|
98
|
+
GUARDRAIL_TOOL="browser"
|
|
99
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // "")}')"
|
|
100
|
+
;;
|
|
101
|
+
Task | TaskCreate | TaskUpdate | TaskStop | Agent | Skill | EnterWorktree)
|
|
102
|
+
GUARDRAIL_TOOL="session.create"
|
|
103
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .prompt // "")}')"
|
|
104
|
+
;;
|
|
105
|
+
CronCreate | CronDelete)
|
|
106
|
+
GUARDRAIL_TOOL="cron"
|
|
107
|
+
CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .schedule // "")}')"
|
|
108
|
+
;;
|
|
109
|
+
mcp__*)
|
|
110
|
+
GUARDRAIL_TOOL="mcp.tool"
|
|
111
|
+
CONTEXT_JSON="$TOOL_INPUT"
|
|
112
|
+
;;
|
|
113
|
+
unknown | *)
|
|
114
|
+
# Unknown tool: fail-closed (deny)
|
|
115
|
+
deny "🛡️ APort: unknown tool '$TOOL_NAME' — fail-closed policy"
|
|
116
|
+
;;
|
|
117
|
+
esac
|
|
118
|
+
|
|
119
|
+
# Use a per-invocation decision file to avoid race conditions with concurrent tool calls
|
|
120
|
+
HOOK_DECISION_FILE="${OPENCLAW_DECISION_FILE:-}"
|
|
121
|
+
if [ -n "$HOOK_DECISION_FILE" ]; then
|
|
122
|
+
HOOK_DECISION_FILE="${HOOK_DECISION_FILE%.json}-$$.json"
|
|
123
|
+
export OPENCLAW_DECISION_FILE="$HOOK_DECISION_FILE"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Call core evaluator (guardrail expects tool name, not policy ID)
|
|
127
|
+
set +e
|
|
128
|
+
"$GUARDRAIL" "$GUARDRAIL_TOOL" "$CONTEXT_JSON" 2> /dev/null
|
|
129
|
+
GUARDRAIL_EXIT=$?
|
|
130
|
+
set -e
|
|
131
|
+
|
|
132
|
+
# Clean up per-invocation decision file on exit
|
|
133
|
+
cleanup_decision() { [ -n "$HOOK_DECISION_FILE" ] && rm -f "$HOOK_DECISION_FILE" 2> /dev/null; }
|
|
134
|
+
|
|
135
|
+
if [ "$GUARDRAIL_EXIT" -eq 0 ]; then
|
|
136
|
+
cleanup_decision
|
|
137
|
+
exit 0
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Deny: read reason from decision file
|
|
141
|
+
REASON="Policy denied this action."
|
|
142
|
+
if [ -n "$HOOK_DECISION_FILE" ] && [ -f "$HOOK_DECISION_FILE" ] && command -v jq &> /dev/null; then
|
|
143
|
+
R="$(jq -r '.reasons[0].message // empty' "$HOOK_DECISION_FILE" 2> /dev/null)"
|
|
144
|
+
[ -n "$R" ] && REASON="$R"
|
|
145
|
+
fi
|
|
146
|
+
cleanup_decision
|
|
147
|
+
deny "🛡️ APort: $REASON"
|
|
@@ -503,6 +503,11 @@ else
|
|
|
503
503
|
EOF
|
|
504
504
|
fi
|
|
505
505
|
|
|
506
|
+
# Archive existing passport before overwrite
|
|
507
|
+
if [ -f "$PASSPORT_FILE" ]; then
|
|
508
|
+
cp "$PASSPORT_FILE" "${PASSPORT_FILE}.bak"
|
|
509
|
+
fi
|
|
510
|
+
|
|
506
511
|
# Format JSON with jq if available
|
|
507
512
|
if command -v jq &> /dev/null; then
|
|
508
513
|
jq . "$PASSPORT_FILE.tmp" > "$PASSPORT_FILE"
|
|
@@ -44,8 +44,9 @@ if [ -n "$DEBUG_APORT" ]; then
|
|
|
44
44
|
echo "DEBUG: CONTEXT length=${#CONTEXT_JSON}" >&2
|
|
45
45
|
fi
|
|
46
46
|
|
|
47
|
-
# Ensure APort data dir exists (for decision.json, audit.log)
|
|
47
|
+
# Ensure APort data dir exists (for decision.json, audit.log) with restricted permissions
|
|
48
48
|
mkdir -p "$(dirname "$AUDIT_LOG")"
|
|
49
|
+
chmod 700 "$(dirname "$AUDIT_LOG")" 2> /dev/null || true
|
|
49
50
|
|
|
50
51
|
# Function to load policy from upstream or local-overrides
|
|
51
52
|
load_policy() {
|
|
@@ -160,9 +161,11 @@ write_decision() {
|
|
|
160
161
|
local final_json
|
|
161
162
|
final_json=$(echo "$base_json" | jq -c --arg h "$content_hash" '. + {content_hash: $h}')
|
|
162
163
|
echo "$final_json" > "$DECISION_FILE"
|
|
164
|
+
chmod 600 "$DECISION_FILE" 2> /dev/null || true
|
|
163
165
|
|
|
164
166
|
# Update chain state for next decision (best-effort; do not block or fail the script)
|
|
165
167
|
echo "{\"last_decision_id\":\"$decision_id\",\"last_content_hash\":\"$content_hash\"}" > "$chain_state" 2> /dev/null || true
|
|
168
|
+
chmod 600 "$chain_state" 2> /dev/null || true
|
|
166
169
|
|
|
167
170
|
audit_context=""
|
|
168
171
|
if [ -n "${CONTEXT_SUMMARY:-}" ]; then
|
|
@@ -514,8 +517,9 @@ if [[ "$POLICY_ID" == "data.file.write.v1" ]]; then
|
|
|
514
517
|
fi
|
|
515
518
|
done < <(echo "$LIMITS" | jq -r '.blocked_paths[]? // empty')
|
|
516
519
|
|
|
517
|
-
# Check allowed extensions (
|
|
518
|
-
|
|
520
|
+
# Check allowed extensions (only when the list has entries)
|
|
521
|
+
_ext_count="$(echo "$LIMITS" | jq -r 'if .allowed_extensions then (.allowed_extensions | length) else 0 end' 2> /dev/null || echo "0")"
|
|
522
|
+
if [ "$_ext_count" -gt 0 ] 2> /dev/null; then
|
|
519
523
|
FILE_EXT=$(echo "$FILE_PATH" | grep -o '\.[^.]*$' | tr '[:upper:]' '[:lower:]')
|
|
520
524
|
if [ -n "$FILE_EXT" ]; then
|
|
521
525
|
EXT_ALLOWED=false
|
|
@@ -14,8 +14,24 @@ resolve_aport_paths() {
|
|
|
14
14
|
local passport_path
|
|
15
15
|
local data_dir
|
|
16
16
|
|
|
17
|
+
# Source validation if available (for validate_passport_path)
|
|
18
|
+
local _resolve_script_dir
|
|
19
|
+
_resolve_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
if [ -f "$_resolve_script_dir/lib/validation.sh" ]; then
|
|
21
|
+
# shellcheck source=lib/validation.sh
|
|
22
|
+
. "$_resolve_script_dir/lib/validation.sh" 2> /dev/null || true
|
|
23
|
+
elif [ -f "$_resolve_script_dir/../bin/lib/validation.sh" ]; then
|
|
24
|
+
. "$_resolve_script_dir/../bin/lib/validation.sh" 2> /dev/null || true
|
|
25
|
+
fi
|
|
26
|
+
|
|
17
27
|
# 1) Explicit path set and file exists → use it (plugin or wrapper)
|
|
18
28
|
if [ -n "${OPENCLAW_PASSPORT_FILE:-}" ] && [ -f "$OPENCLAW_PASSPORT_FILE" ]; then
|
|
29
|
+
# Validate env-provided path if validator is available
|
|
30
|
+
if type validate_passport_path &> /dev/null; then
|
|
31
|
+
if ! validate_passport_path "$OPENCLAW_PASSPORT_FILE"; then
|
|
32
|
+
echo "[aport] WARN: OPENCLAW_PASSPORT_FILE path failed validation: $OPENCLAW_PASSPORT_FILE" >&2
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
19
35
|
data_dir="$(dirname "$OPENCLAW_PASSPORT_FILE")"
|
|
20
36
|
passport_path="$OPENCLAW_PASSPORT_FILE"
|
|
21
37
|
# 2) Explicit path set but file missing → legacy: try parent dir (e.g. .../openclaw/passport.json)
|
|
@@ -31,7 +47,7 @@ resolve_aport_paths() {
|
|
|
31
47
|
# 3) No env → probe framework-specific default paths (where each framework stores data), then OpenClaw legacy
|
|
32
48
|
else
|
|
33
49
|
config_dir=""
|
|
34
|
-
for candidate in "$HOME/.cursor" "$HOME/.openclaw" "$HOME/.aport/langchain" "$HOME/.aport/crewai" "$HOME/.n8n"; do
|
|
50
|
+
for candidate in "$HOME/.claude" "$HOME/.cursor" "$HOME/.openclaw" "$HOME/.aport/langchain" "$HOME/.aport/crewai" "$HOME/.n8n"; do
|
|
35
51
|
if [ -f "${candidate}/aport/passport.json" ]; then
|
|
36
52
|
config_dir="$candidate"
|
|
37
53
|
break
|
package/bin/aport-status.sh
CHANGED
|
@@ -218,11 +218,17 @@ if [ -f "$AUDIT_LOG" ] && [ -s "$AUDIT_LOG" ]; then
|
|
|
218
218
|
# Parse log line: [timestamp] tool=X decision_id=Y allow=Z ... context="..." (optional)
|
|
219
219
|
timestamp=$(echo "$line" | sed -n 's/.*\[\([^]]*\)\].*/\1/p')
|
|
220
220
|
tool=$(echo "$line" | sed -n 's/.*tool=\([^ ]*\).*/\1/p')
|
|
221
|
+
decision_id=$(echo "$line" | sed -n 's/.*decision_id=\([^ ]*\).*/\1/p')
|
|
221
222
|
allow=$(echo "$line" | sed -n 's/.*allow=\([^ ]*\).*/\1/p')
|
|
222
223
|
context=$(echo "$line" | sed -n 's/.*context="\([^"]*\)".*/\1/p')
|
|
223
224
|
timestamp=${timestamp:-unknown}
|
|
224
225
|
tool=${tool:-unknown}
|
|
225
226
|
allow=${allow:-unknown}
|
|
227
|
+
# Synthetic decision_id for old log formats that lack one
|
|
228
|
+
if [ -z "$decision_id" ]; then
|
|
229
|
+
ctx_slug=$(printf '%.16s' "${context:-}" | tr ' ' '_')
|
|
230
|
+
decision_id="${timestamp}-${tool}-${ctx_slug}"
|
|
231
|
+
fi
|
|
226
232
|
|
|
227
233
|
short_time=$(echo "$timestamp" | cut -d' ' -f1-2 | cut -d'.' -f1)
|
|
228
234
|
# Show capability + context when present (e.g. "exec.run | cat test.md"); truncate long context
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Claude Code framework installer/setup.
|
|
3
|
+
# Runs passport wizard and writes ~/.claude/settings.json with PreToolUse hook
|
|
4
|
+
# pointing at aport-claude-code-hook.sh. Format is Claude Code official schema
|
|
5
|
+
|
|
6
|
+
LIB="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")/../lib" && pwd)"
|
|
7
|
+
# shellcheck source=../lib/common.sh
|
|
8
|
+
source "$LIB/common.sh"
|
|
9
|
+
# shellcheck source=../lib/passport.sh
|
|
10
|
+
source "$LIB/passport.sh"
|
|
11
|
+
# shellcheck source=../lib/config.sh
|
|
12
|
+
source "$LIB/config.sh"
|
|
13
|
+
|
|
14
|
+
run_setup() {
|
|
15
|
+
log_info "Setting up APort guardrails for Claude Code..."
|
|
16
|
+
config_dir="$(get_config_dir claude-code)"
|
|
17
|
+
config_dir="${config_dir/#\~/$HOME}"
|
|
18
|
+
mkdir -p "$config_dir/aport"
|
|
19
|
+
chmod 700 "$config_dir/aport"
|
|
20
|
+
|
|
21
|
+
export APORT_FRAMEWORK=claude-code
|
|
22
|
+
run_passport_wizard "$@"
|
|
23
|
+
|
|
24
|
+
# Harden permissions on passport (contains policy/capabilities)
|
|
25
|
+
[ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
|
|
26
|
+
|
|
27
|
+
# Resolve absolute path to hook script (works from repo or npx package)
|
|
28
|
+
HOOK_SCRIPT="${APORT_CLAUDE_CODE_HOOK_SCRIPT:-}"
|
|
29
|
+
if [ -z "$HOOK_SCRIPT" ]; then
|
|
30
|
+
ROOT_FOR_HOOK="$(cd "$LIB/../.." && pwd)"
|
|
31
|
+
HOOK_SCRIPT="$ROOT_FOR_HOOK/bin/aport-claude-code-hook.sh"
|
|
32
|
+
fi
|
|
33
|
+
if [ ! -f "$HOOK_SCRIPT" ]; then
|
|
34
|
+
log_warn "Hook script not found at $HOOK_SCRIPT; settings.json will reference it (create the file for hooks to work)."
|
|
35
|
+
else
|
|
36
|
+
HOOK_SCRIPT="$(cd "$(dirname "$HOOK_SCRIPT")" && pwd)/$(basename "$HOOK_SCRIPT")"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
CLAUDE_DIR="${APORT_CLAUDE_CODE_CONFIG_DIR:-$HOME/.claude}"
|
|
40
|
+
CLAUDE_DIR="${CLAUDE_DIR/#\~/$HOME}"
|
|
41
|
+
SETTINGS_FILE="$CLAUDE_DIR/settings.json"
|
|
42
|
+
mkdir -p "$CLAUDE_DIR"
|
|
43
|
+
|
|
44
|
+
_write_claude_settings "$SETTINGS_FILE" "$HOOK_SCRIPT"
|
|
45
|
+
chmod 600 "$SETTINGS_FILE"
|
|
46
|
+
|
|
47
|
+
echo ""
|
|
48
|
+
echo " Next steps (Claude Code):"
|
|
49
|
+
echo " ─────────────────────────"
|
|
50
|
+
echo " 1. Settings written to: $SETTINGS_FILE"
|
|
51
|
+
echo " 2. Hook script: $HOOK_SCRIPT"
|
|
52
|
+
echo " 3. Restart Claude Code so the PreToolUse hook is picked up."
|
|
53
|
+
echo " 4. Tool use will be checked by APort policy (exit 2 = block)."
|
|
54
|
+
echo ""
|
|
55
|
+
echo " Audit log: $config_dir/aport/audit.log"
|
|
56
|
+
echo ""
|
|
57
|
+
echo " Note: claude --dangerously-skip-permissions bypasses ALL hooks including APort."
|
|
58
|
+
echo " See: docs/frameworks/claude-code.md"
|
|
59
|
+
echo ""
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Write ~/.claude/settings.json in Claude Code official format (PreToolUse, matcher "*").
|
|
63
|
+
# Merges with existing settings.json without clobbering other settings.
|
|
64
|
+
_write_claude_settings() {
|
|
65
|
+
local file="$1"
|
|
66
|
+
local cmd="$2"
|
|
67
|
+
|
|
68
|
+
if [ -f "$file" ] && command -v jq &> /dev/null; then
|
|
69
|
+
if jq -e '.hooks' "$file" &> /dev/null; then
|
|
70
|
+
# Merge: add APort to PreToolUse array, dedup by command
|
|
71
|
+
local tmpfile
|
|
72
|
+
tmpfile="$(mktemp "${file}.XXXXXX")"
|
|
73
|
+
jq -c --arg cmd "$cmd" '
|
|
74
|
+
(.hooks.PreToolUse // []) as $p |
|
|
75
|
+
.hooks.PreToolUse = ($p | map(select(
|
|
76
|
+
(.hooks[0].command != $cmd)
|
|
77
|
+
)) | . + [{"matcher":"*","hooks":[{"type":"command","command":$cmd}]}])
|
|
78
|
+
' "$file" > "$tmpfile" && mv "$tmpfile" "$file"
|
|
79
|
+
return
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Write fresh settings (Claude Code format: hooks.PreToolUse, matcher "*")
|
|
84
|
+
if command -v jq &> /dev/null; then
|
|
85
|
+
jq -n --arg cmd "$cmd" '{
|
|
86
|
+
hooks: {
|
|
87
|
+
PreToolUse: [{"matcher":"*","hooks":[{"type":"command","command":$cmd}]}]
|
|
88
|
+
}
|
|
89
|
+
}' > "$file"
|
|
90
|
+
else
|
|
91
|
+
# Escape special JSON characters in cmd path (quotes, backslashes)
|
|
92
|
+
local escaped_cmd
|
|
93
|
+
escaped_cmd="$(printf '%s' "$cmd" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
|
94
|
+
cat > "$file" << EOF
|
|
95
|
+
{
|
|
96
|
+
"hooks": {
|
|
97
|
+
"PreToolUse": [
|
|
98
|
+
{
|
|
99
|
+
"matcher": "*",
|
|
100
|
+
"hooks": [
|
|
101
|
+
{
|
|
102
|
+
"type": "command",
|
|
103
|
+
"command": "${escaped_cmd}"
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
EOF
|
|
111
|
+
fi
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
run_setup "$@"
|
package/bin/frameworks/crewai.sh
CHANGED
|
@@ -14,8 +14,11 @@ run_setup() {
|
|
|
14
14
|
log_info "Setting up APort guardrails for CrewAI..."
|
|
15
15
|
config_dir="$(write_config_template crewai)"
|
|
16
16
|
mkdir -p "$config_dir/aport"
|
|
17
|
+
chmod 700 "$config_dir/aport"
|
|
17
18
|
export APORT_FRAMEWORK=crewai
|
|
18
19
|
run_passport_wizard "$@"
|
|
20
|
+
# Harden permissions on passport (contains policy/capabilities)
|
|
21
|
+
[ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
|
|
19
22
|
echo ""
|
|
20
23
|
echo " Next steps (CrewAI):"
|
|
21
24
|
echo " ───────────────────"
|
package/bin/frameworks/cursor.sh
CHANGED
|
@@ -16,10 +16,14 @@ run_setup() {
|
|
|
16
16
|
# Passport and data live under Cursor's config dir (~/.cursor/aport/ by default).
|
|
17
17
|
config_dir="$(get_config_dir cursor)"
|
|
18
18
|
mkdir -p "$config_dir/aport"
|
|
19
|
+
chmod 700 "$config_dir/aport"
|
|
19
20
|
|
|
20
21
|
export APORT_FRAMEWORK=cursor
|
|
21
22
|
run_passport_wizard "$@"
|
|
22
23
|
|
|
24
|
+
# Harden permissions on passport (contains policy/capabilities)
|
|
25
|
+
[ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
|
|
26
|
+
|
|
23
27
|
# Resolve absolute path to hook script (works from repo or npx package)
|
|
24
28
|
HOOK_SCRIPT="${APORT_CURSOR_HOOK_SCRIPT:-}"
|
|
25
29
|
if [ -z "$HOOK_SCRIPT" ]; then
|
|
@@ -48,6 +52,7 @@ run_setup() {
|
|
|
48
52
|
.hooks.beforeShellExecution = ($b | map(select(.command != $cmd)) | . + [{ "command": $cmd }]) |
|
|
49
53
|
.hooks.preToolUse = ($p | map(select(.command != $cmd)) | . + [{ "command": $cmd }])
|
|
50
54
|
')
|
|
55
|
+
[ -f "$CURSOR_HOOKS_FILE" ] && cp "$CURSOR_HOOKS_FILE" "${CURSOR_HOOKS_FILE}.bak"
|
|
51
56
|
echo "$NEW_HOOKS" > "$CURSOR_HOOKS_FILE"
|
|
52
57
|
else
|
|
53
58
|
_write_cursor_hooks_file "$CURSOR_HOOKS_FILE" "$HOOK_SCRIPT"
|
|
@@ -55,6 +60,7 @@ run_setup() {
|
|
|
55
60
|
else
|
|
56
61
|
_write_cursor_hooks_file "$CURSOR_HOOKS_FILE" "$HOOK_SCRIPT"
|
|
57
62
|
fi
|
|
63
|
+
chmod 600 "$CURSOR_HOOKS_FILE"
|
|
58
64
|
|
|
59
65
|
echo ""
|
|
60
66
|
echo " Next steps (Cursor):"
|
|
@@ -64,7 +70,7 @@ run_setup() {
|
|
|
64
70
|
echo " 3. Restart Cursor (or reload window) so hooks are picked up."
|
|
65
71
|
echo " 4. Shell commands and tool use will be checked by APort policy (exit 2 = block)."
|
|
66
72
|
echo ""
|
|
67
|
-
echo " Same script works for VS Code + Copilot
|
|
73
|
+
echo " Same script works for VS Code + Copilot. For Claude Code use the dedicated integration: docs/frameworks/claude-code.md"
|
|
68
74
|
echo ""
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -80,12 +86,14 @@ _write_cursor_hooks_file() {
|
|
|
80
86
|
}
|
|
81
87
|
}' > "$file"
|
|
82
88
|
else
|
|
89
|
+
local escaped_cmd
|
|
90
|
+
escaped_cmd="$(printf '%s' "$cmd" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
|
83
91
|
cat > "$file" << EOF
|
|
84
92
|
{
|
|
85
93
|
"version": 1,
|
|
86
94
|
"hooks": {
|
|
87
|
-
"beforeShellExecution": [{"command": "$
|
|
88
|
-
"preToolUse": [{"command": "$
|
|
95
|
+
"beforeShellExecution": [{"command": "${escaped_cmd}"}],
|
|
96
|
+
"preToolUse": [{"command": "${escaped_cmd}"}]
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
EOF
|
|
@@ -14,8 +14,11 @@ run_setup() {
|
|
|
14
14
|
log_info "Setting up APort guardrails for LangChain..."
|
|
15
15
|
config_dir="$(write_config_template langchain)"
|
|
16
16
|
mkdir -p "$config_dir/aport"
|
|
17
|
+
chmod 700 "$config_dir/aport"
|
|
17
18
|
export APORT_FRAMEWORK=langchain
|
|
18
19
|
run_passport_wizard "$@"
|
|
20
|
+
# Harden permissions on passport (contains policy/capabilities)
|
|
21
|
+
[ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
|
|
19
22
|
echo ""
|
|
20
23
|
echo " Next steps (LangChain):"
|
|
21
24
|
echo " ───────────────────────"
|
package/bin/frameworks/n8n.sh
CHANGED
|
@@ -20,8 +20,11 @@ run_setup() {
|
|
|
20
20
|
log_info "Setting up APort guardrails for n8n..."
|
|
21
21
|
config_dir="$(write_config_template n8n)"
|
|
22
22
|
mkdir -p "$config_dir/aport"
|
|
23
|
+
chmod 700 "$config_dir/aport"
|
|
23
24
|
export APORT_FRAMEWORK=n8n
|
|
24
25
|
run_passport_wizard "$@"
|
|
26
|
+
# Harden permissions on passport (contains policy/capabilities)
|
|
27
|
+
[ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
|
|
25
28
|
echo ""
|
|
26
29
|
echo " Next steps (n8n):"
|
|
27
30
|
echo " ────────────────"
|
package/bin/lib/allowlist.sh
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
# shellcheck source=./common.sh
|
|
6
6
|
source "$(dirname "${BASH_SOURCE[0]:-.}")/common.sh"
|
|
7
7
|
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# Stub: command allowlist checking is implemented directly in aport-guardrail-bash.sh
|
|
9
|
+
# (safe_prefix_match + blocked_patterns). This file is kept for backward compatibility
|
|
10
|
+
# but callers MUST NOT rely on this function for security enforcement.
|
|
10
11
|
check_command_allowed() {
|
|
11
12
|
local command_line="$1"
|
|
12
13
|
local allowed_list="${2:-*}"
|
|
13
|
-
# TODO: Implement against passport allowed_commands + blocked patterns
|
|
14
14
|
[[ -z "$command_line" ]] && return 1
|
|
15
|
-
|
|
15
|
+
# SECURITY: Not implemented here — use aport-guardrail-bash.sh for actual enforcement
|
|
16
|
+
echo "[aport] WARN: check_command_allowed is a stub; use aport-guardrail-bash.sh" >&2
|
|
17
|
+
return 1
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export -f check_command_allowed
|
package/bin/lib/config.sh
CHANGED
|
@@ -15,6 +15,7 @@ get_config_dir() {
|
|
|
15
15
|
crewai) echo "${APORT_CREWAI_CONFIG_DIR:-$HOME/.aport/crewai}" ;;
|
|
16
16
|
n8n) echo "${APORT_N8N_CONFIG_DIR:-$HOME/.n8n}" ;;
|
|
17
17
|
cursor) echo "${APORT_CURSOR_CONFIG_DIR:-$HOME/.cursor}" ;;
|
|
18
|
+
claude-code) echo "${APORT_CLAUDE_CODE_CONFIG_DIR:-$HOME/.claude}" ;;
|
|
18
19
|
*) echo "${APORT_CONFIG_DIR:-$HOME/.aport}" ;;
|
|
19
20
|
esac
|
|
20
21
|
}
|
package/bin/lib/detect.sh
CHANGED
|
@@ -32,7 +32,12 @@ detect_frameworks_list() {
|
|
|
32
32
|
grep -qi 'crewai' "$dir/requirements.txt" 2> /dev/null && list+=(crewai)
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
-
#
|
|
35
|
+
# Claude Code: detect claude binary or ~/.claude directory
|
|
36
|
+
if command -v claude &> /dev/null || [[ -d "$HOME/.claude" ]]; then
|
|
37
|
+
list+=(claude-code)
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Dedupe preserving order
|
|
36
41
|
local seen=() out=()
|
|
37
42
|
if [[ ${#list[@]} -gt 0 ]]; then
|
|
38
43
|
for fw in "${list[@]}"; do
|