@garethdaine/agentops 0.9.0 → 0.9.1
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/hooks/auto-delegate.sh +3 -3
- package/hooks/auto-evolve.sh +1 -1
- package/hooks/auto-verify.sh +1 -1
- package/hooks/content-trust.sh +1 -0
- package/hooks/credential-redact.sh +23 -2
- package/hooks/exfiltration-check.sh +22 -8
- package/hooks/flag-utils.sh +14 -3
- package/hooks/hooks.json +12 -1
- package/hooks/patterns-lib.sh +4 -1
- package/hooks/redact-lib.sh +9 -1
- package/hooks/telemetry.sh +45 -13
- package/hooks/todo-prune.sh +4 -3
- package/hooks/unicode-firewall.sh +20 -0
- package/hooks/unicode-lib.sh +26 -32
- package/hooks/validate-command.sh +10 -2
- package/hooks/validate-env.sh +2 -1
- package/hooks/validate-path.sh +5 -7
- package/package.json +5 -1
- package/settings.json +12 -1
package/hooks/auto-delegate.sh
CHANGED
|
@@ -3,7 +3,7 @@ set -uo pipefail
|
|
|
3
3
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
4
|
source "${SCRIPT_DIR}/feature-flags.sh"
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
agentops_automation_enabled 'auto_delegate_enabled' || exit 0
|
|
7
7
|
|
|
8
8
|
INPUT=$(cat) || exit 0
|
|
9
9
|
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) || exit 0
|
|
@@ -33,8 +33,8 @@ CODE_TRACKER="${STATE_DIR}/modified-files.txt"
|
|
|
33
33
|
# Count source code files only
|
|
34
34
|
CODE_COUNT=$(sort -u "$CODE_TRACKER" 2>/dev/null | grep -cE "$SOURCE_CODE_EXTENSIONS" || echo 0)
|
|
35
35
|
|
|
36
|
-
# After
|
|
37
|
-
if [ "$CODE_COUNT" -ge
|
|
36
|
+
# After threshold source code files modified, trigger delegation
|
|
37
|
+
if [ "$CODE_COUNT" -ge "$AGENTOPS_DELEGATE_THRESHOLD" ]; then
|
|
38
38
|
date -u +%FT%TZ > "$DELEGATE_SENT" 2>/dev/null
|
|
39
39
|
|
|
40
40
|
# Collect the modified source files for review context
|
package/hooks/auto-evolve.sh
CHANGED
|
@@ -3,7 +3,7 @@ set -uo pipefail
|
|
|
3
3
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
4
|
source "${SCRIPT_DIR}/feature-flags.sh"
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
agentops_automation_enabled 'auto_evolve_enabled' || exit 0
|
|
7
7
|
|
|
8
8
|
INPUT=$(cat) || exit 0
|
|
9
9
|
CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null) || CWD="."
|
package/hooks/auto-verify.sh
CHANGED
|
@@ -3,7 +3,7 @@ set -uo pipefail
|
|
|
3
3
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
4
|
source "${SCRIPT_DIR}/feature-flags.sh"
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
agentops_automation_enabled 'auto_verify_enabled' || exit 0
|
|
7
7
|
|
|
8
8
|
INPUT=$(cat) || exit 0
|
|
9
9
|
CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null) || CWD="."
|
package/hooks/content-trust.sh
CHANGED
|
@@ -7,12 +7,33 @@ source "${SCRIPT_DIR}/feature-flags.sh"
|
|
|
7
7
|
|
|
8
8
|
INPUT=$(cat) || exit 0
|
|
9
9
|
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) || exit 0
|
|
10
|
+
|
|
11
|
+
SENSITIVE_EXTENSIONS='\.(env|pem|key|crt|secret|p12|pfx|credential|token|password|ssh)'
|
|
12
|
+
SENSITIVE_CONFIG='\.(json|yaml|yml|toml|cfg|ini)$'
|
|
13
|
+
|
|
14
|
+
# Handle Read tool — warn when reading sensitive files
|
|
15
|
+
if [ "$TOOL" = "Read" ]; then
|
|
16
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null) || exit 0
|
|
17
|
+
if echo "$FILE_PATH" | grep -qiE "$SENSITIVE_EXTENSIONS"; then
|
|
18
|
+
LOG_DIR="${CLAUDE_PROJECT_DIR:-.}/.agentops"
|
|
19
|
+
mkdir -p "$LOG_DIR" 2>/dev/null
|
|
20
|
+
jq -nc --arg ts "$(date -u +%FT%TZ)" --arg fp "$FILE_PATH" \
|
|
21
|
+
'{ts:$ts, event:"CREDENTIAL_ACCESS", tool:"Read", file:$fp}' >> "$LOG_DIR/audit.jsonl" 2>/dev/null
|
|
22
|
+
|
|
23
|
+
jq -nc '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:"WARNING: This file may contain credentials. Do NOT include any secrets, API keys, tokens, or passwords in your response. Redact any sensitive values with [REDACTED]."}}'
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Handle Bash tool — warn when commands read credential files
|
|
10
30
|
[ "$TOOL" != "Bash" ] && exit 0
|
|
11
31
|
|
|
12
32
|
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || exit 0
|
|
13
33
|
|
|
14
|
-
#
|
|
15
|
-
if echo "$COMMAND" | grep -qiE "(cat|less|head|more|tail|grep
|
|
34
|
+
# Match: reading credential files via common tools or scripting languages
|
|
35
|
+
if echo "$COMMAND" | grep -qiE "(cat|less|head|more|tail|grep|jq|python3?|ruby|node|source|base64|xxd)\s.*${SENSITIVE_EXTENSIONS}" || \
|
|
36
|
+
echo "$COMMAND" | grep -qiE "source\s+.*\.env\b"; then
|
|
16
37
|
LOG_DIR="${CLAUDE_PROJECT_DIR:-.}/.agentops"
|
|
17
38
|
mkdir -p "$LOG_DIR" 2>/dev/null
|
|
18
39
|
REDACTED_CMD=$(echo "$COMMAND" | agentops_redact)
|
|
@@ -12,14 +12,20 @@ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) || agentops_fail
|
|
|
12
12
|
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || agentops_fail_closed
|
|
13
13
|
[ -z "$COMMAND" ] && exit 0
|
|
14
14
|
|
|
15
|
+
# Normalize command: strip path prefixes so /usr/bin/curl matches as curl
|
|
16
|
+
NORM_CMD=$(echo "$COMMAND" | sed -E 's|/[^ ]*/([^ /]+)|\1|g')
|
|
17
|
+
|
|
15
18
|
HARD_DENY=$(agentops_hard_deny)
|
|
16
19
|
SENSITIVE_FILES='\.(env|pem|key|crt|p12|pfx|secret|credential|token|password|ssh)'
|
|
17
20
|
|
|
21
|
+
# Network tool pattern — covers common and uncommon transfer utilities
|
|
22
|
+
NET_TOOLS='(curl|wget|nc|ncat|socat|scp|rsync|sftp|ssh|http|httpie|aria2c|openssl\s+s_client|telnet|lwp-request|fetch)'
|
|
23
|
+
|
|
18
24
|
# Hard-deny rules — always enforce, even in bypass/unrestricted mode
|
|
19
25
|
|
|
20
26
|
# 1. Network transfer of sensitive files (including -F for curl form uploads)
|
|
21
|
-
if echo "$
|
|
22
|
-
if echo "$
|
|
27
|
+
if echo "$NORM_CMD" | grep -qE "curl.*(-d|--data|--upload-file|-F)|wget.*--post|nc\s|ncat\s|socat\s|http\s+(POST|PUT|PATCH)|aria2c\s|openssl\s+s_client"; then
|
|
28
|
+
if echo "$NORM_CMD" | grep -qiE "$SENSITIVE_FILES"; then
|
|
23
29
|
jq -nc --arg action "$HARD_DENY" \
|
|
24
30
|
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$action,permissionDecisionReason:"ExfiltrationDetector: network transfer of sensitive file type (hard deny)"}}'
|
|
25
31
|
exit 0
|
|
@@ -27,15 +33,15 @@ if echo "$COMMAND" | grep -qE "curl.*(-d|--data|--upload-file|-F)|wget.*--post|n
|
|
|
27
33
|
fi
|
|
28
34
|
|
|
29
35
|
# 2. Piping secrets to network (including command substitution: curl -d "$(cat .env)")
|
|
30
|
-
if echo "$
|
|
31
|
-
echo "$
|
|
36
|
+
if echo "$NORM_CMD" | grep -qE "(cat|less|head|tail|base64|xxd).*${SENSITIVE_FILES}.*\|.*${NET_TOOLS}" || \
|
|
37
|
+
echo "$NORM_CMD" | grep -qE "${NET_TOOLS}.*\\$\(.*${SENSITIVE_FILES}"; then
|
|
32
38
|
jq -nc --arg action "$HARD_DENY" \
|
|
33
39
|
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$action,permissionDecisionReason:"ExfiltrationDetector: piping sensitive file to network command (hard deny)"}}'
|
|
34
40
|
exit 0
|
|
35
41
|
fi
|
|
36
42
|
|
|
37
43
|
# 3. Direct file transfer tools with sensitive files
|
|
38
|
-
if echo "$
|
|
44
|
+
if echo "$NORM_CMD" | grep -qE "(scp|rsync|sftp)\s" && echo "$NORM_CMD" | grep -qiE "$SENSITIVE_FILES"; then
|
|
39
45
|
jq -nc --arg action "$HARD_DENY" \
|
|
40
46
|
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$action,permissionDecisionReason:"ExfiltrationDetector: file transfer of sensitive file type (hard deny)"}}'
|
|
41
47
|
exit 0
|
|
@@ -46,22 +52,30 @@ agentops_is_bypass "$INPUT" && agentops_bypass_advisory "exfiltration-check"
|
|
|
46
52
|
ACTION=$(agentops_enforcement_action)
|
|
47
53
|
|
|
48
54
|
# 4. Base64 encoding + network (obfuscation attempt)
|
|
49
|
-
if echo "$
|
|
55
|
+
if echo "$NORM_CMD" | grep -qE "base64.*\|.*${NET_TOOLS}" || echo "$NORM_CMD" | grep -qE "${NET_TOOLS}.*base64"; then
|
|
50
56
|
jq -nc --arg action "$ACTION" \
|
|
51
57
|
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$action,permissionDecisionReason:"ExfiltrationDetector: base64 encoding with network transfer detected"}}'
|
|
52
58
|
exit 0
|
|
53
59
|
fi
|
|
54
60
|
|
|
55
61
|
# 5. DNS exfiltration via command substitution (including backticks)
|
|
56
|
-
if echo "$
|
|
62
|
+
if echo "$NORM_CMD" | grep -qE "(dig|nslookup|host)\s.*(\\$\(|\`)" ; then
|
|
57
63
|
jq -nc --arg action "$ACTION" \
|
|
58
64
|
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$action,permissionDecisionReason:"ExfiltrationDetector: possible DNS exfiltration via command substitution"}}'
|
|
59
65
|
exit 0
|
|
60
66
|
fi
|
|
61
67
|
|
|
62
68
|
# 6. Scripting language network calls with sensitive file references
|
|
63
|
-
if echo "$
|
|
69
|
+
if echo "$NORM_CMD" | grep -qE "(python3?|ruby|node|perl)\s" && echo "$NORM_CMD" | grep -qiE "(requests\.(post|put|patch)|urllib|http\.request|Net::HTTP|fetch|open\()" && echo "$NORM_CMD" | grep -qiE "$SENSITIVE_FILES"; then
|
|
64
70
|
jq -nc --arg action "$ACTION" \
|
|
65
71
|
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$action,permissionDecisionReason:"ExfiltrationDetector: scripting language network call with sensitive file reference"}}'
|
|
66
72
|
exit 0
|
|
67
73
|
fi
|
|
74
|
+
|
|
75
|
+
# 7. Script-write-then-execute pattern (write a script and immediately run it)
|
|
76
|
+
if echo "$NORM_CMD" | grep -qE "(echo|cat|printf|tee).*>.*\.(sh|py|rb|pl|js)\s*[;&|]" && \
|
|
77
|
+
echo "$NORM_CMD" | grep -qE "(bash|sh|python3?|ruby|perl|node)\s"; then
|
|
78
|
+
jq -nc --arg action "$ACTION" \
|
|
79
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$action,permissionDecisionReason:"ExfiltrationDetector: script-write-then-execute pattern detected — review for exfiltration"}}'
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
package/hooks/flag-utils.sh
CHANGED
|
@@ -4,13 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
FLAGS_FILE="${CLAUDE_PROJECT_DIR:-.}/.agentops/flags.json"
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# Internal cache — avoids re-reading flags.json on every agentops_flag call.
|
|
8
|
+
_AGENTOPS_FLAGS_CACHE=""
|
|
9
|
+
_AGENTOPS_FLAGS_LOADED=false
|
|
10
|
+
|
|
11
|
+
# Read a single flag value from flags.json (cached after first read).
|
|
8
12
|
# Usage: VALUE=$(agentops_flag "flag_name" "default")
|
|
9
13
|
agentops_flag() {
|
|
10
14
|
local FLAG="$1"
|
|
11
15
|
local DEFAULT="${2:-true}"
|
|
12
|
-
if [
|
|
13
|
-
|
|
16
|
+
if [ "$_AGENTOPS_FLAGS_LOADED" = false ]; then
|
|
17
|
+
if [ -f "$FLAGS_FILE" ]; then
|
|
18
|
+
_AGENTOPS_FLAGS_CACHE=$(cat "$FLAGS_FILE" 2>/dev/null) || true
|
|
19
|
+
fi
|
|
20
|
+
_AGENTOPS_FLAGS_LOADED=true
|
|
21
|
+
fi
|
|
22
|
+
if [ -n "$_AGENTOPS_FLAGS_CACHE" ]; then
|
|
23
|
+
echo "$_AGENTOPS_FLAGS_CACHE" | jq -r --arg key "$FLAG" --arg def "$DEFAULT" \
|
|
24
|
+
'if .[$key] == null then $def else (.[$key] | tostring) end' 2>/dev/null || echo "$DEFAULT"
|
|
14
25
|
else
|
|
15
26
|
echo "$DEFAULT"
|
|
16
27
|
fi
|
package/hooks/hooks.json
CHANGED
|
@@ -48,6 +48,12 @@
|
|
|
48
48
|
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/injection-scan.sh", "timeout": 5 },
|
|
49
49
|
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/runtime-mode.sh", "timeout": 5 }
|
|
50
50
|
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"matcher": "Bash|Write|Edit",
|
|
54
|
+
"hooks": [
|
|
55
|
+
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/unicode-firewall.sh", "timeout": 5 }
|
|
56
|
+
]
|
|
51
57
|
}
|
|
52
58
|
],
|
|
53
59
|
"PostToolUse": [
|
|
@@ -75,10 +81,15 @@
|
|
|
75
81
|
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/integrity-verify.sh", "timeout": 5 }
|
|
76
82
|
]
|
|
77
83
|
},
|
|
84
|
+
{
|
|
85
|
+
"matcher": "Bash|Read",
|
|
86
|
+
"hooks": [
|
|
87
|
+
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/credential-redact.sh", "timeout": 5 }
|
|
88
|
+
]
|
|
89
|
+
},
|
|
78
90
|
{
|
|
79
91
|
"matcher": "Bash",
|
|
80
92
|
"hooks": [
|
|
81
|
-
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/credential-redact.sh", "timeout": 5 },
|
|
82
93
|
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/detect-test-run.sh", "timeout": 5 }
|
|
83
94
|
]
|
|
84
95
|
},
|
package/hooks/patterns-lib.sh
CHANGED
|
@@ -19,4 +19,7 @@ AGENTOPS_PROTECTED_PATHS='(\.agentops/|tasks/lessons\.md$)'
|
|
|
19
19
|
|
|
20
20
|
# Writable state files — whitelisted from protected path enforcement
|
|
21
21
|
# so that plugin commands (e.g. /agentops:flags) can manage them via Write/Edit.
|
|
22
|
-
AGENTOPS_WRITABLE_STATE='(\.agentops/flags\.json$|\.agentops/
|
|
22
|
+
AGENTOPS_WRITABLE_STATE='(\.agentops/flags\.json$|\.agentops/build-state\.json$|\.agentops/build-execution\.jsonl$|tasks/lessons\.md$)'
|
|
23
|
+
|
|
24
|
+
# Delegation threshold — number of modified source files before auto-delegate triggers
|
|
25
|
+
AGENTOPS_DELEGATE_THRESHOLD=5
|
package/hooks/redact-lib.sh
CHANGED
|
@@ -8,8 +8,16 @@ agentops_redact() {
|
|
|
8
8
|
-e 's/(PASSWORD|PASS|SECRET|TOKEN|API_KEY|PRIVATE_KEY|AUTH|CREDENTIAL)=[^ "'\''&]*/\1=[REDACTED]/gi' \
|
|
9
9
|
-e 's/(sk|pk|api|key|token|secret|auth)[-_][A-Za-z0-9]{16,}/[REDACTED]/g' \
|
|
10
10
|
-e 's/Bearer [A-Za-z0-9._~+\/=-]{20,}/Bearer [REDACTED]/g' \
|
|
11
|
+
-e 's/Basic [A-Za-z0-9+\/=]{20,}/Basic [REDACTED]/g' \
|
|
11
12
|
-e 's/AKIA[A-Z0-9]{16}/[REDACTED]/g' \
|
|
12
13
|
-e 's/gh[pousr]_[A-Za-z0-9]{36,}/[REDACTED]/g' \
|
|
14
|
+
-e 's/xox[bpas]-[A-Za-z0-9-]{10,}/[REDACTED]/g' \
|
|
15
|
+
-e 's/sk_(live|test)_[A-Za-z0-9]{10,}/[REDACTED]/g' \
|
|
16
|
+
-e 's/pk_(live|test)_[A-Za-z0-9]{10,}/[REDACTED]/g' \
|
|
17
|
+
-e 's/sk-ant-[A-Za-z0-9_-]{10,}/[REDACTED]/g' \
|
|
18
|
+
-e 's|hooks\.slack\.com/services/[A-Za-z0-9/]+|hooks.slack.com/services/[REDACTED]|g' \
|
|
19
|
+
-e 's|discord(app)?\.com/api/webhooks/[A-Za-z0-9/]+|discord.com/api/webhooks/[REDACTED]|g' \
|
|
13
20
|
-e 's|[a-zA-Z]+://[^:@/]+:[^@/]+@|[REDACTED_CONN]@|g' \
|
|
14
|
-
-e 's/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/[REDACTED_JWT]/g'
|
|
21
|
+
-e 's/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/[REDACTED_JWT]/g' \
|
|
22
|
+
-e 's/-----BEGIN[A-Z ]*PRIVATE KEY-----/[REDACTED_PEM]/g'
|
|
15
23
|
}
|
package/hooks/telemetry.sh
CHANGED
|
@@ -10,24 +10,56 @@ TS=$(date -u +%FT%TZ)
|
|
|
10
10
|
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "unknown"' 2>/dev/null) || EVENT="unknown"
|
|
11
11
|
SESSION=$(echo "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null) || SESSION="unknown"
|
|
12
12
|
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null) || TOOL=""
|
|
13
|
-
CWD=$(echo "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
|
|
14
13
|
|
|
15
14
|
jq -nc \
|
|
16
15
|
--arg ts "$TS" --arg event "$EVENT" --arg session "$SESSION" \
|
|
17
|
-
--arg tool "$TOOL"
|
|
18
|
-
'{ts:$ts, event:$event, session:$session, tool:$tool
|
|
16
|
+
--arg tool "$TOOL" \
|
|
17
|
+
'{ts:$ts, event:$event, session:$session, tool:$tool}' >> "$LOG_FILE" 2>/dev/null || true
|
|
19
18
|
|
|
20
|
-
# Forward to OTLP if configured — validate endpoint
|
|
19
|
+
# Forward to OTLP if configured — validate endpoint with hostname allowlist
|
|
21
20
|
if [ -n "${OTLP_ENDPOINT:-}" ]; then
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
# Require explicit hostname allowlist — reject if not configured
|
|
22
|
+
OTLP_ALLOWLIST="${OTLP_HOSTNAME_ALLOWLIST:-}"
|
|
23
|
+
if [ -z "$OTLP_ALLOWLIST" ]; then
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Extract hostname from endpoint URL
|
|
28
|
+
OTLP_HOST=$(echo "$OTLP_ENDPOINT" | sed -E 's|^https?://([^:/]+).*|\1|')
|
|
29
|
+
|
|
30
|
+
# Must be HTTPS
|
|
31
|
+
if ! echo "$OTLP_ENDPOINT" | grep -qE '^https://'; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Check hostname against allowlist (comma-separated)
|
|
36
|
+
ALLOWED=false
|
|
37
|
+
IFS=',' read -ra HOSTS <<< "$OTLP_ALLOWLIST"
|
|
38
|
+
for ALLOWED_HOST in "${HOSTS[@]}"; do
|
|
39
|
+
ALLOWED_HOST=$(echo "$ALLOWED_HOST" | tr -d ' ')
|
|
40
|
+
if [ "$OTLP_HOST" = "$ALLOWED_HOST" ]; then
|
|
41
|
+
ALLOWED=true
|
|
42
|
+
break
|
|
43
|
+
fi
|
|
44
|
+
done
|
|
45
|
+
[ "$ALLOWED" = false ] && exit 0
|
|
46
|
+
|
|
47
|
+
# Resolve hostname and reject private/loopback IPs (DNS rebinding defense)
|
|
48
|
+
RESOLVED_IP=$(dig +short "$OTLP_HOST" 2>/dev/null | head -1)
|
|
49
|
+
if echo "$RESOLVED_IP" | grep -qE '^(127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.|::1|0\.0\.0\.0|fe80:)'; then
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
PAYLOAD=$(tail -1 "$LOG_FILE" 2>/dev/null) || true
|
|
54
|
+
if [ -n "$PAYLOAD" ]; then
|
|
55
|
+
AUTH_HEADER=""
|
|
56
|
+
if [ -n "${OTLP_AUTH_TOKEN:-}" ]; then
|
|
57
|
+
AUTH_HEADER="-H \"Authorization: Bearer ${OTLP_AUTH_TOKEN}\""
|
|
31
58
|
fi
|
|
59
|
+
eval curl -sf --max-time 10 --connect-timeout 5 --no-location \
|
|
60
|
+
-X POST "$OTLP_ENDPOINT/v1/logs" \
|
|
61
|
+
-H "Content-Type: application/json" \
|
|
62
|
+
$AUTH_HEADER \
|
|
63
|
+
-d "'$PAYLOAD'" '&>/dev/null &'
|
|
32
64
|
fi
|
|
33
65
|
fi
|
package/hooks/todo-prune.sh
CHANGED
|
@@ -19,7 +19,8 @@ TODO="${CWD}/tasks/todo.md"
|
|
|
19
19
|
|
|
20
20
|
# Build a pruned version: keep unchecked items and their nearest heading
|
|
21
21
|
# Remove checked items (- [x]) and blank lines that result from removal
|
|
22
|
-
|
|
22
|
+
# Create temp file in same directory as target for atomic mv (same filesystem)
|
|
23
|
+
TEMP=$(mktemp "${TODO}.XXXXXX")
|
|
23
24
|
trap 'rm -f "$TEMP"' EXIT
|
|
24
25
|
|
|
25
26
|
HAS_UNCHECKED=false
|
|
@@ -67,9 +68,9 @@ if [ "$SECTION_HAS_UNCHECKED" = true ] && [ -n "$SECTION_BUFFER" ]; then
|
|
|
67
68
|
fi
|
|
68
69
|
|
|
69
70
|
if [ "$HAS_UNCHECKED" = true ]; then
|
|
70
|
-
# Replace with pruned version containing only incomplete items
|
|
71
|
-
mv "$TEMP" "$TODO"
|
|
71
|
+
# Replace with pruned version containing only incomplete items (atomic rename)
|
|
72
72
|
trap - EXIT
|
|
73
|
+
mv "$TEMP" "$TODO"
|
|
73
74
|
REMAINING=$(grep -cE '^\s*- \[ \]' "$TODO" 2>/dev/null || echo 0)
|
|
74
75
|
jq -nc --arg remaining "$REMAINING" \
|
|
75
76
|
'{systemMessage: ("AgentOps: Pruned completed todos from previous session. " + $remaining + " incomplete item(s) remain in tasks/todo.md — review them before planning new work.")}'
|
|
@@ -21,6 +21,26 @@ audit() {
|
|
|
21
21
|
'{ts:$ts, event:$ev} + $ARGS.named' >> "$LOG_DIR/audit.jsonl" 2>/dev/null
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
# ── PreToolUse: Scan inputs for dangerous Unicode before execution ────────
|
|
25
|
+
if [ "$EVENT" = "PreToolUse" ]; then
|
|
26
|
+
CONTENT=""
|
|
27
|
+
case "$TOOL" in
|
|
28
|
+
Bash) CONTENT=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) ;;
|
|
29
|
+
Write) CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null) ;;
|
|
30
|
+
Edit) CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null) ;;
|
|
31
|
+
esac
|
|
32
|
+
|
|
33
|
+
if [ -n "$CONTENT" ] && echo "$CONTENT" | unicode_detect; then
|
|
34
|
+
CATEGORIES=$(echo "$CONTENT" | unicode_classify)
|
|
35
|
+
audit "UNICODE_PRETOOL_WARNING" --arg tool "$TOOL" --arg cats "$CATEGORIES"
|
|
36
|
+
|
|
37
|
+
jq -nc --arg cats "$CATEGORIES" --arg tool "$TOOL" \
|
|
38
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:("UNICODE FIREWALL: " + $tool + " input contains dangerous invisible Unicode (" + $cats + "). This may be a Trojan Source attack. Review the content carefully before allowing.")}}'
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
24
44
|
# ── PostToolUse: Auto-strip on Write/Edit ───────────────────────────────────
|
|
25
45
|
# Instead of blocking the write, we let it through and immediately sanitise
|
|
26
46
|
# the file, emitting a warning so the agent knows what happened.
|
package/hooks/unicode-lib.sh
CHANGED
|
@@ -10,19 +10,22 @@
|
|
|
10
10
|
# Cat 5: Variation sel. supp. U+E0100-E01EF
|
|
11
11
|
UNICODE_PATTERN='[\x{200B}-\x{200F}\x{2060}-\x{2064}\x{FEFF}\x{202A}-\x{202E}\x{2066}-\x{2069}\x{FE00}-\x{FE0F}\x{E0001}-\x{E007F}\x{E0100}-\x{E01EF}]'
|
|
12
12
|
|
|
13
|
-
# Returns 0 (match) if dangerous invisible Unicode is found
|
|
13
|
+
# Returns 0 (match) if dangerous invisible Unicode is found.
|
|
14
|
+
# Usage: unicode_detect < file OR echo "$text" | unicode_detect
|
|
15
|
+
# unicode_detect "filepath"
|
|
14
16
|
unicode_detect() {
|
|
15
|
-
|
|
17
|
+
if [ $# -gt 0 ]; then
|
|
18
|
+
perl -CSD -ne "if (/$UNICODE_PATTERN/) { exit 0 } END { exit 1 }" "$1" 2>/dev/null
|
|
19
|
+
else
|
|
20
|
+
perl -CSD -ne "if (/$UNICODE_PATTERN/) { exit 0 } END { exit 1 }" 2>/dev/null
|
|
21
|
+
fi
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
# Returns
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
perl -CSD -ne "if (/$UNICODE_PATTERN/) { exit 0 } END { exit 1 }" "$FILE" 2>/dev/null
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
# Returns human-readable category summary from stdin.
|
|
24
|
+
# Returns human-readable category summary.
|
|
25
|
+
# Usage: unicode_classify < file OR echo "$text" | unicode_classify
|
|
26
|
+
# unicode_classify "filepath"
|
|
25
27
|
unicode_classify() {
|
|
28
|
+
local _ARGS=("$@")
|
|
26
29
|
perl -CSD -ne '
|
|
27
30
|
BEGIN { %c = () }
|
|
28
31
|
$c{"zero-width chars"}++ if /[\x{200B}-\x{200F}\x{2060}-\x{2064}\x{FEFF}]/;
|
|
@@ -31,32 +34,18 @@ unicode_classify() {
|
|
|
31
34
|
$c{"tag characters"}++ if /[\x{E0001}-\x{E007F}]/;
|
|
32
35
|
$c{"variation sel. supplement"}++ if /[\x{E0100}-\x{E01EF}]/;
|
|
33
36
|
END { print join(", ", sort keys %c) if %c }
|
|
34
|
-
' 2>/dev/null
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
# Returns human-readable category summary from a file.
|
|
38
|
-
unicode_classify_file() {
|
|
39
|
-
local FILE="$1"
|
|
40
|
-
perl -CSD -ne '
|
|
41
|
-
BEGIN { %c = () }
|
|
42
|
-
$c{"zero-width chars"}++ if /[\x{200B}-\x{200F}\x{2060}-\x{2064}\x{FEFF}]/;
|
|
43
|
-
$c{"bidi overrides"}++ if /[\x{202A}-\x{202E}\x{2066}-\x{2069}]/;
|
|
44
|
-
$c{"variation selectors"}++ if /[\x{FE00}-\x{FE0F}]/;
|
|
45
|
-
$c{"tag characters"}++ if /[\x{E0001}-\x{E007F}]/;
|
|
46
|
-
$c{"variation sel. supplement"}++ if /[\x{E0100}-\x{E01EF}]/;
|
|
47
|
-
END { print join(", ", sort keys %c) if %c }
|
|
48
|
-
' "$FILE" 2>/dev/null
|
|
37
|
+
' "${_ARGS[@]}" 2>/dev/null
|
|
49
38
|
}
|
|
50
39
|
|
|
51
|
-
# Count affected lines
|
|
40
|
+
# Count affected lines.
|
|
41
|
+
# Usage: unicode_count_lines < file OR echo "$text" | unicode_count_lines
|
|
42
|
+
# unicode_count_lines "filepath"
|
|
52
43
|
unicode_count_lines() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
local FILE="$1"
|
|
59
|
-
perl -CSD -ne "print if /$UNICODE_PATTERN/" "$FILE" 2>/dev/null | wc -l | tr -d ' '
|
|
44
|
+
if [ $# -gt 0 ]; then
|
|
45
|
+
perl -CSD -ne "print if /$UNICODE_PATTERN/" "$1" 2>/dev/null | wc -l | tr -d ' '
|
|
46
|
+
else
|
|
47
|
+
perl -CSD -ne "print if /$UNICODE_PATTERN/" 2>/dev/null | wc -l | tr -d ' '
|
|
48
|
+
fi
|
|
60
49
|
}
|
|
61
50
|
|
|
62
51
|
# Strip dangerous Unicode from a file in-place.
|
|
@@ -64,3 +53,8 @@ unicode_strip_file() {
|
|
|
64
53
|
local FILE="$1"
|
|
65
54
|
perl -CSD -pi -e "s/$UNICODE_PATTERN//g" "$FILE" 2>/dev/null
|
|
66
55
|
}
|
|
56
|
+
|
|
57
|
+
# Legacy aliases for backward compatibility with unicode-scan-session.sh
|
|
58
|
+
unicode_detect_file() { unicode_detect "$1"; }
|
|
59
|
+
unicode_classify_file() { unicode_classify "$1"; }
|
|
60
|
+
unicode_count_lines_file() { unicode_count_lines "$1"; }
|
|
@@ -13,7 +13,7 @@ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || a
|
|
|
13
13
|
HARD_DENY=$(agentops_hard_deny)
|
|
14
14
|
|
|
15
15
|
# 1. Destructive system commands (always deny, even in bypass/unrestricted)
|
|
16
|
-
if echo "$COMMAND" | grep -qE "(rm\s+-rf\s+/[^t]|mkfs\s|dd\s+if=|shutdown|reboot|init\s+[06])"; then
|
|
16
|
+
if echo "$COMMAND" | grep -qE "(rm\s+-rf\s+/([^t]|t[^m]|tm[^p]|tmp[^/])|mkfs\s|dd\s+if=|shutdown|reboot|init\s+[06])"; then
|
|
17
17
|
jq -nc --arg action "$HARD_DENY" \
|
|
18
18
|
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$action,permissionDecisionReason:"Destructive system command blocked by AgentOps CommandPolicy (hard deny)"}}'
|
|
19
19
|
exit 0
|
|
@@ -51,7 +51,7 @@ if echo "$COMMAND" | grep -qE '>\s*[^ ]*\.agentops/' \
|
|
|
51
51
|
fi
|
|
52
52
|
fi
|
|
53
53
|
|
|
54
|
-
# 4b. Tampering with hooks/ directory via Bash (always deny — prevent hook tampering)
|
|
54
|
+
# 4b. Tampering with hooks/ directory via Bash write operations (always deny — prevent hook tampering)
|
|
55
55
|
if echo "$COMMAND" | grep -qE '>\s*[^ ]*hooks/' \
|
|
56
56
|
|| echo "$COMMAND" | grep -qE '(tee|cp|mv|install)\s+[^ ]*\s+[^ ]*hooks/' \
|
|
57
57
|
|| echo "$COMMAND" | grep -qE 'sed\s+-i[^ ]*\s+[^ ]*hooks/' \
|
|
@@ -63,6 +63,14 @@ if echo "$COMMAND" | grep -qE '>\s*[^ ]*hooks/' \
|
|
|
63
63
|
exit 0
|
|
64
64
|
fi
|
|
65
65
|
|
|
66
|
+
# 4c. Scripting language file I/O to protected paths (always deny — prevent bypass via python/ruby/node)
|
|
67
|
+
if echo "$COMMAND" | grep -qE "(python3?|ruby|node|perl)\s+(-c|(-e\s))" && \
|
|
68
|
+
echo "$COMMAND" | grep -qE '(\.agentops/|hooks/)'; then
|
|
69
|
+
jq -nc --arg action "$HARD_DENY" \
|
|
70
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$action,permissionDecisionReason:"Scripting language referencing protected paths blocked by AgentOps CommandPolicy (hard deny)"}}'
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
66
74
|
# Soft rules below — bypass/unrestricted can downgrade these
|
|
67
75
|
agentops_is_bypass "$INPUT" && agentops_bypass_advisory "validate-command"
|
|
68
76
|
ACTION=$(agentops_enforcement_action)
|
package/hooks/validate-env.sh
CHANGED
|
@@ -22,7 +22,8 @@ for VAR in $VARS; do
|
|
|
22
22
|
VAR_UPPER=$(echo "$VAR" | tr '[:lower:]' '[:upper:]')
|
|
23
23
|
|
|
24
24
|
# Forbidden keys — LD_PRELOAD, PATH, HOME etc. are always dangerous
|
|
25
|
-
if echo "$VAR_UPPER" | grep -qE "^(PATH|HOME|SHELL|USER|LD_PRELOAD|LD_LIBRARY_PATH|
|
|
25
|
+
if echo "$VAR_UPPER" | grep -qE "^(PATH|HOME|SHELL|USER|LD_PRELOAD|LD_LIBRARY_PATH|NODE_OPTIONS|NODE_PATH|PYTHONPATH|PYTHONSTARTUP|RUBYOPT|PERL5OPT|CLASSPATH)$" || \
|
|
26
|
+
echo "$VAR_UPPER" | grep -qE "^DYLD_"; then
|
|
26
27
|
jq -nc --arg var "$VAR" --arg action "$HARD_DENY" \
|
|
27
28
|
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$action,permissionDecisionReason:("EnvPolicy: forbidden env var " + $var + " (hard deny)")}}'
|
|
28
29
|
exit 0
|
package/hooks/validate-path.sh
CHANGED
|
@@ -38,14 +38,12 @@ if [ ${#FILE_PATH} -gt 1024 ]; then
|
|
|
38
38
|
exit 0
|
|
39
39
|
fi
|
|
40
40
|
|
|
41
|
-
# Canonicalize for symlink resolution (
|
|
41
|
+
# Canonicalize for symlink resolution (resolve existing paths for all tools)
|
|
42
42
|
CANONICAL="$FILE_PATH"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
CANONICAL="$RESOLVED"
|
|
48
|
-
fi
|
|
43
|
+
PARENT_DIR=$(dirname "$FILE_PATH")
|
|
44
|
+
if [ -d "$PARENT_DIR" ]; then
|
|
45
|
+
RESOLVED=$(realpath -m "$FILE_PATH" 2>/dev/null) || RESOLVED="$FILE_PATH"
|
|
46
|
+
CANONICAL="$RESOLVED"
|
|
49
47
|
fi
|
|
50
48
|
|
|
51
49
|
# 4. Protect plugin state files from agent writes (hard deny)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@garethdaine/agentops",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Enterprise guardrails, delivery lifecycle, and self-evolution for Claude Code CLI",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Gareth Daine",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"compliance",
|
|
28
28
|
"audit"
|
|
29
29
|
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "bats tests/",
|
|
32
|
+
"lint": "shellcheck hooks/*.sh"
|
|
33
|
+
},
|
|
30
34
|
"files": [
|
|
31
35
|
".claude-plugin/",
|
|
32
36
|
"agents/",
|
package/settings.json
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [],
|
|
4
|
-
"deny": [
|
|
4
|
+
"deny": [
|
|
5
|
+
"Bash(rm -rf /*)",
|
|
6
|
+
"Bash(curl*|*bash)",
|
|
7
|
+
"Bash(curl*|*sh)",
|
|
8
|
+
"Bash(wget*|*bash)",
|
|
9
|
+
"Bash(wget*|*sh)",
|
|
10
|
+
"Write(/etc/*)",
|
|
11
|
+
"Write(/sys/*)",
|
|
12
|
+
"Write(/proc/*)",
|
|
13
|
+
"Write(/dev/*)",
|
|
14
|
+
"Write(/boot/*)"
|
|
15
|
+
]
|
|
5
16
|
}
|
|
6
17
|
}
|