Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions Releases/v4.0.3+/.claude/PAI/PAISECURITYSYSTEM/patterns.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# PAI Security Patterns - EXAMPLE TEMPLATE
# Single source of truth for security validation
# Used by SecurityValidator.hook.ts
#
# INSTRUCTIONS:
# 1. Copy this file to USER/PAISECURITYSYSTEM/patterns.yaml
# 2. Customize patterns for your environment
# 3. The hook will load USER patterns first, falling back to this example
---
version: "1.0"
last_updated: "2026-01-14"

# Philosophy: Safe but functional by default
# Block catastrophic operations, confirm dangerous ones, allow everything else
philosophy:
mode: safe_functional
principle: "Meaningful protection without friction that drives people to disable security"

# ========================================
# BASH COMMAND PATTERNS
# ========================================
bash:
# BLOCKED - Catastrophic/irreversible (exit 2)
# These operations are NEVER allowed - they cause irreversible damage
blocked:
# Filesystem destruction
- pattern: "rm -rf /"
reason: "Filesystem destruction"
- pattern: "rm -rf ~"
reason: "Home directory destruction"
- pattern: "sudo rm -rf /"
reason: "Filesystem destruction with sudo"
- pattern: "sudo rm -rf ~"
reason: "Home directory destruction with sudo"

# Disk operations
- pattern: "diskutil eraseDisk"
reason: "Disk destruction"
- pattern: "diskutil zeroDisk"
reason: "Disk destruction"
- pattern: "diskutil partitionDisk"
reason: "Disk partitioning"
- pattern: "diskutil apfs deleteContainer"
reason: "APFS container deletion"
- pattern: "diskutil apfs eraseVolume"
reason: "Volume destruction"
- pattern: "dd if=/dev/zero"
reason: "Disk overwrite"
- pattern: "mkfs"
reason: "Filesystem format"

# Repository operations
- pattern: "gh repo delete"
reason: "Repository deletion"
- pattern: "gh repo edit --visibility public"
reason: "Repository exposure"

# CONFIRM - Dangerous but sometimes legitimate (exit 0 + JSON prompt)
# User will be prompted to confirm before execution
confirm:
# Git operations
- pattern: "git push --force"
reason: "Force push can lose commits"
- pattern: "git push -f"
reason: "Force push can lose commits"
- pattern: "git push origin --force"
reason: "Force push can lose commits"
- pattern: "git push origin -f"
reason: "Force push can lose commits"
- pattern: "git reset --hard"
reason: "Loses uncommitted changes"

# Cloud - AWS
- pattern: "aws s3 rm.*--recursive"
reason: "Bulk S3 deletion"
- pattern: "aws ec2 terminate"
reason: "EC2 instance termination"
- pattern: "aws rds delete"
reason: "RDS deletion"

# Cloud - GCP
- pattern: "gcloud.*delete"
reason: "GCP resource deletion"

# Infrastructure as Code
- pattern: "terraform destroy"
reason: "Infrastructure destruction"
- pattern: "terraform apply.*-auto-approve"
reason: "Auto-approve bypasses review"
- pattern: "pulumi destroy"
reason: "Infrastructure destruction"

# Containers
- pattern: "docker system prune"
reason: "Container/image cleanup"
- pattern: "docker volume rm"
reason: "Volume data deletion"
- pattern: "kubectl delete namespace"
reason: "Namespace deletion"

# Databases
- pattern: "DELETE FROM.*WHERE"
reason: "Database deletion (confirm scope)"
- pattern: "DROP DATABASE"
reason: "Database destruction"
- pattern: "DROP TABLE"
reason: "Table destruction"
- pattern: "TRUNCATE"
reason: "Table data destruction"

# ALERT - Suspicious but allowed (log only, no block)
# These are logged for security review but not blocked
alert:
- pattern: "curl.*\\|.*sh"
reason: "Piping curl to shell"
- pattern: "curl.*\\|.*bash"
reason: "Piping curl to bash"
- pattern: "wget.*\\|.*sh"
reason: "Piping wget to shell"
- pattern: "wget.*\\|.*bash"
reason: "Piping wget to bash"

# ========================================
# PATH PROTECTION
# ========================================
paths:
# ZERO ACCESS - Complete denial for all operations (read/write/delete)
# These paths contain secrets that should never be accessed
zeroAccess:
- "~/.ssh/id_*"
- "~/.ssh/*.pem"
- "~/.aws/credentials"
- "~/.gnupg/private*"
- "**/credentials.json"
- "**/service-account*.json"

# READ ONLY - Can read but cannot modify or delete
readOnly:
- "/etc/**"

# CONFIRM WRITE - Can read freely, writing requires confirmation
confirmWrite:
- "**/.env"
- "**/.env.*"
- "~/.ssh/*"

# NO DELETE - Can read and modify but cannot delete
# Add paths to important configuration or infrastructure files
noDelete:
- ".git/**"
- "LICENSE*"
- "README.md"

# ========================================
# SPECIAL PROJECT RULES
# ========================================
# Add project-specific rules here
# Example:
# projects:
# my-project:
# path: "~/Projects/my-project"
# rules:
# - action: "block_git_push"
# reason: "Deploy via custom method only"
projects: {}
8 changes: 8 additions & 0 deletions Releases/v4.0.3+/.claude/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "pai-runtime",
"private": true,
"description": "PAI runtime — manifest enabling hook scripts under ~/.pai/hooks/ to resolve native deps via Bun's parent-directory module resolution. Lands at ~/.pai/package.json; bun install creates ~/.pai/node_modules/ which the hooks then walk up to find.",
"dependencies": {
"yaml": "^2.0.0"
}
}
93 changes: 93 additions & 0 deletions Tools/verify-security-validator.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# verify-security-validator.sh
#
# Behaviour-level smoke test for SecurityValidator.hook.ts. Proves that the
# hook actually blocks dangerous patterns rather than silently fail-opening.
# Issue #158 background: the hook went silently no-op from v4.0.0+ because
# (a) the `yaml` package wasn't resolvable from ~/.pai/hooks/ and
# (b) patterns.example.yaml was no longer shipped. This script is the
# regression guard.
#
# Usage: bash Tools/verify-security-validator.sh
# Exit: 0 if all checks pass, non-zero otherwise.
#
# Payloads are routed via tmp-file stdin so no command line literally
# contains a blocked-pattern substring (otherwise the wrapper command
# itself trips the hook before the test runs).

set -u

PAI_DIR="${PAI_DIR:-$HOME/.pai}"
HOOK="$PAI_DIR/hooks/SecurityValidator.hook.ts"
PATTERNS="$PAI_DIR/PAI/PAISECURITYSYSTEM/patterns.example.yaml"
PKG_JSON="$PAI_DIR/package.json"
YAML_MOD="$PAI_DIR/node_modules/yaml"

PASS=0
FAIL=0
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT

green() { printf '\033[32m%s\033[0m' "$1"; }
red() { printf '\033[31m%s\033[0m' "$1"; }
ok() { printf ' [%s] %s\n' "$(green PASS)" "$1"; PASS=$((PASS+1)); }
ng() { printf ' [%s] %s\n' "$(red FAIL)" "$1"; FAIL=$((FAIL+1)); }

run_hook() {
bun run "$HOOK" < "$1" 2>&1
}

echo
echo "── Prerequisites ──────────────────────────────────────"
[ -f "$HOOK" ] && ok "hook present at $HOOK" || { ng "hook missing"; echo; exit 1; }
[ -f "$PATTERNS" ] && ok "patterns at $PATTERNS" || ng "patterns.example.yaml missing"
[ -f "$PKG_JSON" ] && ok "package.json at $PKG_JSON" || ng "package.json missing"
[ -d "$YAML_MOD" ] && ok "yaml node_module at $YAML_MOD" || ng "node_modules/yaml/ missing — run: cd $PAI_DIR && bun install"

echo
echo "── Behaviour tests ────────────────────────────────────"

# T1 — rm -rf / blocks (exit 2). Build the command via concat so this script's
# own command line never contains the literal blocked substring (which would
# trip the hook before the test could run).
python3 -c 'import json; print(json.dumps({"session_id":"verify","tool_name":"Bash","tool_input":{"command":"r"+"m -rf /"}}))' > "$TMPDIR/t1.json"
out=$(run_hook "$TMPDIR/t1.json"); rc=$?
if [ "$rc" = "2" ] && echo "$out" | grep -q "BLOCKED"; then
ok "rm -rf / -> exit 2 (BLOCKED)"
else
ng "rm -rf / expected exit 2 BLOCKED, got rc=$rc out=$out"
fi

# T2 — Read of ~/.ssh/id_rsa blocks (exit 2)
python3 -c 'import os,json; print(json.dumps({"session_id":"verify","tool_name":"Read","tool_input":{"file_path": os.path.expanduser("~")+"/.ssh/id_rsa"}}))' \
> "$TMPDIR/t2.json"
out=$(run_hook "$TMPDIR/t2.json"); rc=$?
if [ "$rc" = "2" ] && echo "$out" | grep -q "Zero access"; then
ok "Read ~/.ssh/id_rsa -> exit 2 (Zero access)"
else
ng "Read ~/.ssh/id_rsa expected exit 2, got rc=$rc out=$out"
fi

# T3 — safe command continues (exit 0, {continue:true})
printf '%s' '{"session_id":"verify","tool_name":"Bash","tool_input":{"command":"echo hello"}}' > "$TMPDIR/t3.json"
out=$(run_hook "$TMPDIR/t3.json"); rc=$?
if [ "$rc" = "0" ] && echo "$out" | grep -q '"continue":true'; then
ok "echo hello -> exit 0 {continue:true}"
else
ng "echo hello expected exit 0 continue, got rc=$rc out=$out"
fi

# T4 — git push --force prompts (exit 0, {decision:"ask"})
printf '%s' '{"session_id":"verify","tool_name":"Bash","tool_input":{"command":"git push --force"}}' > "$TMPDIR/t4.json"
out=$(run_hook "$TMPDIR/t4.json"); rc=$?
if [ "$rc" = "0" ] && echo "$out" | grep -q '"decision":"ask"'; then
ok "git push --force -> exit 0 {decision:ask}"
else
ng "git push --force expected exit 0 ask, got rc=$rc out=$out"
fi

echo
echo "── Result ─────────────────────────────────────────────"
echo " PASS=$PASS FAIL=$FAIL"
echo
[ "$FAIL" = "0" ]