pantheon-opencode 1.2.1 → 1.2.2
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/AGENTS.md +9 -0
- package/README.md +12 -13
- package/bin/pantheon-init.mjs +14 -0
- package/opencode.json +1 -1
- package/package.json +13 -4
- package/scripts/changelog-extract.mjs +112 -0
- package/scripts/doctor.mjs +303 -13
- package/scripts/hooks/audit-imports.sh +43 -0
- package/scripts/hooks/format-multi-language.sh +96 -0
- package/scripts/hooks/log-session-start.sh +24 -0
- package/scripts/hooks/on-subagent-delegation-start.sh +91 -0
- package/scripts/hooks/on-subagent-delegation-stop.sh +37 -0
- package/scripts/hooks/run-type-check.sh +21 -0
- package/scripts/hooks/scan-secrets.sh +55 -0
- package/scripts/hooks/validate-post-conditions.sh +21 -0
- package/scripts/hooks/validate-talos-scope.sh +43 -0
- package/scripts/hooks/validate-tool-safety.sh +46 -0
- package/scripts/install/opencode.mjs +58 -29
- package/scripts/install/plugin.mjs +4 -2
- package/scripts/install-mcp.mjs +104 -56
- package/scripts/mcp_persistence_server.py +1 -0
- package/scripts/prune.mjs +359 -0
- package/scripts/version-check.mjs +146 -0
- package/scripts/versioning.mjs +13 -14
- package/src/mcp/requirements-mcp.txt +8 -5
- package/src/mcp/requirements-vision.txt +3 -3
- package/src/plugins/hook-runner.ts +186 -0
- package/src/plugins/pantheon-hooks.ts +153 -0
- package/src/plugins/tui/dist/tui.d.ts +8 -0
- package/src/plugins/tui/dist/tui.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui.js +1198 -0
- package/src/plugins/tui/dist/tui.js.map +1 -0
- package/src/plugins/tui/dist/tui.tsx +1204 -0
- package/src/plugins/tui/package.json +2 -2
- package/src/plugins/tui/src/index.tsx +15 -0
- package/src/mcp/requirements-mcp-core.txt +0 -5
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# audit-imports.sh — Pantheon PostToolUse Import Audit Hook
|
|
3
|
+
# Blocks wildcard imports and flags suspicious patterns.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
FILE_PATH="${FILE_PATH:-${1:-}}"
|
|
7
|
+
|
|
8
|
+
if [[ -z "$FILE_PATH" ]] || [[ ! -f "$FILE_PATH" ]]; then
|
|
9
|
+
# Scan all Python/JS files in recent changes if no file specified
|
|
10
|
+
FILES=$(git diff --name-only HEAD 2>/dev/null | grep -E '\.(py|js|ts|jsx|tsx)$' || true)
|
|
11
|
+
if [[ -z "$FILES" ]]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
else
|
|
15
|
+
FILES="$FILE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
FOUND=0
|
|
19
|
+
for f in $FILES; do
|
|
20
|
+
[[ -f "$f" ]] || continue
|
|
21
|
+
EXT="${f##*.}"
|
|
22
|
+
case "$EXT" in
|
|
23
|
+
py)
|
|
24
|
+
if grep -qE '^from\s+\S+\s+import\s+\*' "$f"; then
|
|
25
|
+
echo "[IMPORT AUDIT] Wildcard import found in $f" >&2
|
|
26
|
+
FOUND=1
|
|
27
|
+
fi
|
|
28
|
+
;;
|
|
29
|
+
js|jsx|ts|tsx)
|
|
30
|
+
# Flag require() without destructuring as suspicious
|
|
31
|
+
if grep -qE 'require\([^)]+\)' "$f"; then
|
|
32
|
+
echo "[IMPORT AUDIT] CommonJS require() found in $f — consider ES modules" >&2
|
|
33
|
+
fi
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
if [[ $FOUND -eq 1 ]]; then
|
|
39
|
+
echo "[IMPORT AUDIT] Fix wildcard imports before committing." >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
exit 0
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# format-multi-language.sh — Pantheon PostToolUse Formatting Hook
|
|
3
|
+
# Runs appropriate formatter on the modified file.
|
|
4
|
+
# Supports both Claude Code stdin protocol and legacy env-var input.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
|
8
|
+
|
|
9
|
+
# ── Claude Code stdin protocol ──────────────────────────────────────────
|
|
10
|
+
# Parse JSON from stdin when piped (PostToolUse hook format).
|
|
11
|
+
# Extracts file_path from Edit/Write or file paths from Bash commands.
|
|
12
|
+
if [ -p /dev/stdin ]; then
|
|
13
|
+
STDIN_JSON=$(cat)
|
|
14
|
+
TOOL_NAME=$(echo "$STDIN_JSON" | python3 -c "
|
|
15
|
+
import sys, json
|
|
16
|
+
d = json.load(sys.stdin)
|
|
17
|
+
print(d.get('tool_name', ''))
|
|
18
|
+
" 2>/dev/null || echo "")
|
|
19
|
+
|
|
20
|
+
case "$TOOL_NAME" in
|
|
21
|
+
Edit|Write)
|
|
22
|
+
JSON_PATH=$(echo "$STDIN_JSON" | python3 -c "
|
|
23
|
+
import sys, json
|
|
24
|
+
d = json.load(sys.stdin)
|
|
25
|
+
print(d.get('tool_input', {}).get('file_path', ''))
|
|
26
|
+
" 2>/dev/null || echo "")
|
|
27
|
+
if [ -n "$JSON_PATH" ]; then
|
|
28
|
+
FILE_PATH="$JSON_PATH"
|
|
29
|
+
fi
|
|
30
|
+
;;
|
|
31
|
+
Bash)
|
|
32
|
+
COMMAND=$(echo "$STDIN_JSON" | python3 -c "
|
|
33
|
+
import sys, json
|
|
34
|
+
d = json.load(sys.stdin)
|
|
35
|
+
print(d.get('tool_input', {}).get('command', ''))
|
|
36
|
+
" 2>/dev/null || echo "")
|
|
37
|
+
if [ -n "$COMMAND" ]; then
|
|
38
|
+
# Extract first file-like path from bash command (supports relative and quoted paths)
|
|
39
|
+
CANDIDATE=$(python3 -c "
|
|
40
|
+
import re, shlex, sys
|
|
41
|
+
command = sys.argv[1]
|
|
42
|
+
try:
|
|
43
|
+
tokens = shlex.split(command)
|
|
44
|
+
except ValueError:
|
|
45
|
+
tokens = command.split()
|
|
46
|
+
path_re = re.compile(r'^([^\\n]+\\.[A-Za-z0-9]+)$')
|
|
47
|
+
for token in tokens:
|
|
48
|
+
if token.startswith('-'):
|
|
49
|
+
continue
|
|
50
|
+
if path_re.match(token):
|
|
51
|
+
print(token)
|
|
52
|
+
break
|
|
53
|
+
" "$COMMAND" 2>/dev/null || echo "")
|
|
54
|
+
if [ -n "$CANDIDATE" ]; then
|
|
55
|
+
FILE_PATH="$CANDIDATE"
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
;;
|
|
59
|
+
esac
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
FILE_PATH="${FILE_PATH:-${1:-}}"
|
|
63
|
+
if [[ -n "$FILE_PATH" ]] && [[ "$FILE_PATH" != /* ]]; then
|
|
64
|
+
if [[ -f "$REPO_ROOT/$FILE_PATH" ]]; then
|
|
65
|
+
FILE_PATH="$REPO_ROOT/$FILE_PATH"
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [[ -z "$FILE_PATH" ]] || [[ ! -f "$FILE_PATH" ]]; then
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
EXT="${FILE_PATH##*.}"
|
|
74
|
+
EXIT_CODE=0
|
|
75
|
+
|
|
76
|
+
case "$EXT" in
|
|
77
|
+
py)
|
|
78
|
+
if command -v ruff >/dev/null 2>&1; then
|
|
79
|
+
ruff format "$FILE_PATH" || true
|
|
80
|
+
elif command -v black >/dev/null 2>&1; then
|
|
81
|
+
black -q "$FILE_PATH" || true
|
|
82
|
+
fi
|
|
83
|
+
;;
|
|
84
|
+
js|jsx|ts|tsx|json|jsonc|md|yaml|yml|css|html)
|
|
85
|
+
if command -v prettier >/dev/null 2>&1; then
|
|
86
|
+
prettier --write "$FILE_PATH" || true
|
|
87
|
+
fi
|
|
88
|
+
;;
|
|
89
|
+
sh|bash)
|
|
90
|
+
if command -v shfmt >/dev/null 2>&1; then
|
|
91
|
+
shfmt -w "$FILE_PATH" || true
|
|
92
|
+
fi
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
|
|
96
|
+
exit 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# log-session-start.sh — Pantheon SessionStart Logging Hook
|
|
3
|
+
# Logs session start with structured JSON.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
# Project-local by default; set XDG_STATE_HOME for system-wide logging
|
|
7
|
+
if [ -n "${LOG_DIR:-}" ]; then
|
|
8
|
+
LOG_DIR="$LOG_DIR"
|
|
9
|
+
elif [ -n "${XDG_STATE_HOME:-}" ]; then
|
|
10
|
+
LOG_DIR="$XDG_STATE_HOME/pantheon/hooks"
|
|
11
|
+
else
|
|
12
|
+
LOG_DIR="logs/agent-sessions"
|
|
13
|
+
fi
|
|
14
|
+
mkdir -p "$LOG_DIR"
|
|
15
|
+
|
|
16
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
17
|
+
SESSION_ID="${SESSION_ID:-$(date +%s)}"
|
|
18
|
+
|
|
19
|
+
cat >> "$LOG_DIR/sessions.log" << JSON
|
|
20
|
+
{"event":"SessionStart","timestamp":"$TIMESTAMP","session_id":"$SESSION_ID","platform":"${PLATFORM:-unknown}"}
|
|
21
|
+
JSON
|
|
22
|
+
|
|
23
|
+
echo "[LOG] Session start logged → $LOG_DIR/sessions.log" >&2
|
|
24
|
+
exit 0
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# on-subagent-delegation-start.sh — Pantheon SubagentStart Audit Hook
|
|
3
|
+
# Logs when Zeus delegates to a subagent.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
# Project-local by default; set XDG_STATE_HOME for system-wide logging
|
|
7
|
+
if [ -n "${LOG_DIR:-}" ]; then
|
|
8
|
+
LOG_DIR="$LOG_DIR"
|
|
9
|
+
elif [ -n "${XDG_STATE_HOME:-}" ]; then
|
|
10
|
+
LOG_DIR="$XDG_STATE_HOME/pantheon/hooks"
|
|
11
|
+
else
|
|
12
|
+
LOG_DIR="logs/agent-sessions"
|
|
13
|
+
fi
|
|
14
|
+
mkdir -p "$LOG_DIR"
|
|
15
|
+
|
|
16
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
17
|
+
AGENT_NAME="${AGENT_NAME:-unknown}"
|
|
18
|
+
TASK_DESC="${TASK_DESC:-${1:-}}"
|
|
19
|
+
|
|
20
|
+
cat >> "$LOG_DIR/delegations.log" << JSON
|
|
21
|
+
{"event":"SubagentStart","timestamp":"$TIMESTAMP","agent":"$AGENT_NAME","task":"$TASK_DESC"}
|
|
22
|
+
JSON
|
|
23
|
+
|
|
24
|
+
echo "[DELEGATION] $AGENT_NAME started → $LOG_DIR/delegations.log" >&2
|
|
25
|
+
|
|
26
|
+
# --- Dispatch-time validation: check target against routing.yml ---
|
|
27
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
|
29
|
+
ROUTING_FILE="$REPO_ROOT/routing.yml"
|
|
30
|
+
if [ -f "$ROUTING_FILE" ] && [ -n "$AGENT_NAME" ]; then
|
|
31
|
+
ESCAPED_AGENT_NAME=$(printf '%s\n' "$AGENT_NAME" | sed 's/[][(){}.^$*+?|\/\\-]/\\&/g')
|
|
32
|
+
if grep -qE "^[[:space:]]*-[[:space:]]+target:[[:space:]]+$ESCAPED_AGENT_NAME[[:space:]]*$" "$ROUTING_FILE" 2>/dev/null; then
|
|
33
|
+
echo "[VALIDATION] ✅ Target '$AGENT_NAME' found in routing.yml" >&2
|
|
34
|
+
else
|
|
35
|
+
echo "[VALIDATION] ⚠️ Target '$AGENT_NAME' NOT in routing.yml delegation rules" >&2
|
|
36
|
+
fi
|
|
37
|
+
elif [ ! -f "$ROUTING_FILE" ]; then
|
|
38
|
+
echo "[VALIDATION] ⚠️ Could not read routing.yml for validation" >&2
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# --- Cost estimation for delegation ---
|
|
42
|
+
declare -A TIER_MAP
|
|
43
|
+
TIER_MAP[athena]="premium"
|
|
44
|
+
TIER_MAP[themis]="premium"
|
|
45
|
+
TIER_MAP[hermes]="default"
|
|
46
|
+
TIER_MAP[aphrodite]="default"
|
|
47
|
+
TIER_MAP[demeter]="default"
|
|
48
|
+
TIER_MAP[prometheus]="default"
|
|
49
|
+
TIER_MAP[hephaestus]="default"
|
|
50
|
+
TIER_MAP[gaia]="default"
|
|
51
|
+
TIER_MAP[zeus]="default"
|
|
52
|
+
TIER_MAP[apollo]="fast"
|
|
53
|
+
TIER_MAP[nyx]="fast"
|
|
54
|
+
TIER_MAP[iris]="fast"
|
|
55
|
+
TIER_MAP[mnemosyne]="fast"
|
|
56
|
+
TIER_MAP[talos]="fast"
|
|
57
|
+
|
|
58
|
+
TIER="${TIER_MAP[$AGENT_NAME]:-default}"
|
|
59
|
+
|
|
60
|
+
case "$TIER" in
|
|
61
|
+
premium)
|
|
62
|
+
COST_MIN=0.50
|
|
63
|
+
COST_MAX=2.00
|
|
64
|
+
COST_EST=1.25
|
|
65
|
+
;;
|
|
66
|
+
fast)
|
|
67
|
+
COST_MIN=0.02
|
|
68
|
+
COST_MAX=0.10
|
|
69
|
+
COST_EST=0.06
|
|
70
|
+
;;
|
|
71
|
+
*)
|
|
72
|
+
COST_MIN=0.10
|
|
73
|
+
COST_MAX=0.50
|
|
74
|
+
COST_EST=0.30
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
|
|
78
|
+
# Format cost with 2 decimal places
|
|
79
|
+
COST_EST_FMT=$(printf "%.2f" "$COST_EST")
|
|
80
|
+
|
|
81
|
+
if [ "$TIER" = "premium" ]; then
|
|
82
|
+
echo "[COST_WARN] High-cost delegation to $AGENT_NAME — estimated \$$COST_EST_FMT" >&2
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
if (( $(echo "$COST_EST > 5.00" | bc -l 2>/dev/null) )); then
|
|
86
|
+
echo "[COST_ALERT] Expensive delegation to $AGENT_NAME — consider if subtask is possible" >&2
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
echo "[COST] Delegation to $AGENT_NAME — tier: $TIER — estimated: \$$COST_EST_FMT" >&2
|
|
90
|
+
|
|
91
|
+
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# on-subagent-delegation-stop.sh — Pantheon SubagentStop Audit Hook
|
|
3
|
+
# Logs completion or failure of subagent tasks.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
# Project-local by default; set XDG_STATE_HOME for system-wide logging
|
|
7
|
+
if [ -n "${LOG_DIR:-}" ]; then
|
|
8
|
+
LOG_DIR="$LOG_DIR"
|
|
9
|
+
elif [ -n "${XDG_STATE_HOME:-}" ]; then
|
|
10
|
+
LOG_DIR="$XDG_STATE_HOME/pantheon/hooks"
|
|
11
|
+
else
|
|
12
|
+
LOG_DIR="logs/agent-sessions"
|
|
13
|
+
fi
|
|
14
|
+
mkdir -p "$LOG_DIR"
|
|
15
|
+
|
|
16
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
17
|
+
AGENT_NAME="${AGENT_NAME:-unknown}"
|
|
18
|
+
STATUS="${STATUS:-success}"
|
|
19
|
+
REASON="${REASON:-${1:-}}"
|
|
20
|
+
|
|
21
|
+
LOG_FILE="$LOG_DIR/delegations.log"
|
|
22
|
+
if [[ "$STATUS" == "failure" ]]; then
|
|
23
|
+
LOG_FILE="$LOG_DIR/delegation-failures.log"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
cat >> "$LOG_FILE" << JSON
|
|
27
|
+
{"event":"SubagentStop","timestamp":"$TIMESTAMP","agent":"$AGENT_NAME","status":"$STATUS","reason":"$REASON"}
|
|
28
|
+
JSON
|
|
29
|
+
|
|
30
|
+
echo "[DELEGATION] $AGENT_NAME stopped ($STATUS) → $LOG_FILE" >&2
|
|
31
|
+
|
|
32
|
+
# --- Post-condition validation: remind about Themis review ---
|
|
33
|
+
if echo "hermes aphrodite demeter prometheus" | grep -wq "$AGENT_NAME" 2>/dev/null; then
|
|
34
|
+
echo "[POST-CONDITION] 🔔 Reminder: '$AGENT_NAME' should call Themis for review after implementation" >&2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
exit 0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# run-type-check.sh — Pantheon PostToolUse Type Check Hook
|
|
3
|
+
# Runs mypy (Python) or tsc (TypeScript) if config exists.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
# Only run if relevant config files exist
|
|
7
|
+
if [[ -f "pyproject.toml" ]] || [[ -f "setup.cfg" ]] || [[ -f ".mypy.ini" ]]; then
|
|
8
|
+
if command -v mypy >/dev/null 2>&1; then
|
|
9
|
+
echo "[TYPE CHECK] Running mypy..." >&2
|
|
10
|
+
mypy . --ignore-missing-imports || true
|
|
11
|
+
fi
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
if [[ -f "tsconfig.json" ]]; then
|
|
15
|
+
if command -v tsc >/dev/null 2>&1; then
|
|
16
|
+
echo "[TYPE CHECK] Running tsc --noEmit..." >&2
|
|
17
|
+
tsc --noEmit || true
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
exit 0
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scan-secrets.sh — Pantheon PreToolUse Secret Scanning Hook
|
|
3
|
+
# Detects hardcoded secrets in tool input.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SECRET_PATTERNS=(
|
|
7
|
+
"AKIA[0-9A-Z]{16}"
|
|
8
|
+
"gh[pousr]_[A-Za-z0-9_]{36,}"
|
|
9
|
+
"glpat-[A-Za-z0-9_\\-]{20}"
|
|
10
|
+
"sk-[a-zA-Z0-9]{20,}"
|
|
11
|
+
"sk_live_[a-zA-Z0-9]{20,}"
|
|
12
|
+
"sk_test_[a-zA-Z0-9]{20,}"
|
|
13
|
+
"xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}"
|
|
14
|
+
"[a-zA-Z0-9_-]*api[_-]?key[a-zA-Z0-9_-]*\s*[:=]\s*[\"']?[a-zA-Z0-9_\\-]{16,}[\"']?"
|
|
15
|
+
"[a-zA-Z0-9_-]*password[a-zA-Z0-9_-]*\s*[:=]\s*[\"'][^\"']{8,}[\"']"
|
|
16
|
+
"[a-zA-Z0-9_-]*secret[a-zA-Z0-9_-]*\s*[:=]\s*[\"'][^\"']{8,}[\"']"
|
|
17
|
+
"bearer\s+[a-zA-Z0-9_\\-\\.]{20,}"
|
|
18
|
+
"eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Keep provider-specific markers assembled so this scanner is not self-matched
|
|
22
|
+
# by the repository secret test.
|
|
23
|
+
BIFROST_HEADER="x""-bf-""vk"
|
|
24
|
+
BIFROST_TOKEN_PREFIX="sk""-bf-"
|
|
25
|
+
SECRET_PATTERNS+=(
|
|
26
|
+
"$BIFROST_HEADER"
|
|
27
|
+
"${BIFROST_TOKEN_PREFIX}[A-Za-z0-9_-]{8,}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
INPUT="${1:-${TOOL_INPUT:-}}"
|
|
31
|
+
if [[ -z "$INPUT" ]] && [[ ! -t 0 ]]; then
|
|
32
|
+
INPUT=$(cat)
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [[ -z "$INPUT" ]]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
FOUND=0
|
|
40
|
+
for pattern in "${SECRET_PATTERNS[@]}"; do
|
|
41
|
+
if echo "$INPUT" | grep -iqE "$pattern"; then
|
|
42
|
+
MATCH=$(echo "$INPUT" | grep -ioE "$pattern" | head -1)
|
|
43
|
+
# Mask the match for safe logging
|
|
44
|
+
MASKED="${MATCH:0:4}****${MATCH: -4}"
|
|
45
|
+
echo "[SECRET SCAN] Potential secret detected: $MASKED (pattern: $pattern)" >&2
|
|
46
|
+
FOUND=1
|
|
47
|
+
fi
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
if [[ $FOUND -eq 1 ]]; then
|
|
51
|
+
echo "[SECRET SCAN] Hardcoded secret detected. Remove it and use environment variables or a vault." >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
exit 0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# validate-post-conditions.sh — Stop hook
|
|
3
|
+
# Checks that Themis review was called after implementation
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
# Read JSON input from stdin
|
|
7
|
+
INPUT=$(cat)
|
|
8
|
+
SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" 2>/dev/null || echo "")
|
|
9
|
+
|
|
10
|
+
REVIEW_DIR=".pantheon/memory-bank/.tmp"
|
|
11
|
+
if [ -d "$REVIEW_DIR" ]; then
|
|
12
|
+
REVIEW_COUNT=$(ls "$REVIEW_DIR"/REVIEW-* 2>/dev/null | wc -l)
|
|
13
|
+
if [ "$REVIEW_COUNT" -eq 0 ]; then
|
|
14
|
+
echo "[POST-CONDITION] ⚠️ No REVIEW artifact found in $REVIEW_DIR/" >&2
|
|
15
|
+
echo "[POST-CONDITION] 💡 Remember: implementation agents MUST call @themis for review" >&2
|
|
16
|
+
else
|
|
17
|
+
echo "[POST-CONDITION] ✅ $REVIEW_COUNT REVIEW artifact(s) found" >&2
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
exit 0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# validate-talos-scope.sh — PreToolUse hook for Talos
|
|
3
|
+
# Receives JSON on stdin, enforces Talos boundaries
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
# Read JSON input from stdin (Claude Code hook protocol)
|
|
7
|
+
INPUT=$(cat)
|
|
8
|
+
|
|
9
|
+
# Extract tool info from JSON input
|
|
10
|
+
TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null || echo "")
|
|
11
|
+
TOOL_INPUT=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps(d.get('tool_input',{})))" 2>/dev/null || echo "")
|
|
12
|
+
|
|
13
|
+
# Only validate for Talos agent
|
|
14
|
+
AGENT_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('agent_id',''))" 2>/dev/null || echo "")
|
|
15
|
+
if [ "$AGENT_NAME" != "talos" ]; then
|
|
16
|
+
# Not Talos — allow pass-through
|
|
17
|
+
echo "[TALOS SCOPE] Not Talos agent, skipping" >&2
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Get changed files from the tool input
|
|
22
|
+
if echo "$TOOL_INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('command','') or d.get('filePath','') or d.get('file_path',''))" 2>/dev/null | grep -qE '(migration|schema\.sql|alembic|models/.*\.py|auth|security|password|token|secret)'; then
|
|
23
|
+
echo "[TALOS SCOPE] ❌ Blocked: Talos cannot modify schema/security files" >&2
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Count files in the edit/write operation
|
|
28
|
+
FILE_COUNT=$(echo "$TOOL_INPUT" | python3 -c "
|
|
29
|
+
import sys, json
|
|
30
|
+
try:
|
|
31
|
+
d = json.load(sys.stdin)
|
|
32
|
+
# Count unique file paths in the input
|
|
33
|
+
files = set()
|
|
34
|
+
for key in ['filePath', 'file_path', 'command', 'content']:
|
|
35
|
+
if key in d and d[key]:
|
|
36
|
+
files.add(str(d[key]))
|
|
37
|
+
print(len(files))
|
|
38
|
+
except:
|
|
39
|
+
print('0')
|
|
40
|
+
" 2>/dev/null || echo "0")
|
|
41
|
+
|
|
42
|
+
echo "[TALOS SCOPE] ✅ Scope check passed" >&2
|
|
43
|
+
exit 0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# validate-tool-safety.sh — Pantheon PreToolUse Security Hook
|
|
3
|
+
# Blocks destructive commands: rm -rf, DROP TABLE, TRUNCATE, etc.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
BLOCKED_PATTERNS=(
|
|
7
|
+
# "rm -rf /" appears mid-JSON in the hook stdin protocol (e.g.
|
|
8
|
+
# {"tool_input":{"command":"rm -rf /"}}), so the old `/$` end-of-line
|
|
9
|
+
# anchor never matched. Match "/" followed by a JSON delimiter (", },
|
|
10
|
+
# comma), whitespace, or end of line — catches bare "rm -rf /" anywhere
|
|
11
|
+
# in the payload while keeping "rm -rf /some/path" (safe) unblocked.
|
|
12
|
+
"rm\s+-rf\s+/([\"},\t[:space:]]|$)" # rm -rf / (root only)
|
|
13
|
+
"rm\s+-rf\s+/\*" # rm -rf /*
|
|
14
|
+
"rm\s+-rf\s+~([\",[:space:]]|$)" # rm -rf ~ (home only)
|
|
15
|
+
"rm\s+-rf\s+~/" # rm -rf ~/...
|
|
16
|
+
"rm\s+-rf\s+\.\.?/" # rm -rf ./ or rm -rf ../
|
|
17
|
+
"DROP\s+TABLE"
|
|
18
|
+
"DROP\s+DATABASE"
|
|
19
|
+
"TRUNCATE\s+TABLE"
|
|
20
|
+
"DELETE\s+FROM\s+.*;?\s*$"
|
|
21
|
+
"rm\s+.*\s+--no-preserve-root"
|
|
22
|
+
":(){ :|:& };:"
|
|
23
|
+
">\s*/dev/\(sd[a-z]\|hd[a-z]\|disk[0-9]*\)"
|
|
24
|
+
"dd\s+if=.*of=/dev/"
|
|
25
|
+
"mkfs\."
|
|
26
|
+
"fdisk\s+/dev/"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
INPUT="${1:-${TOOL_INPUT:-}}"
|
|
30
|
+
if [[ -z "$INPUT" ]] && [[ ! -t 0 ]]; then
|
|
31
|
+
INPUT=$(cat)
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if [[ -z "$INPUT" ]]; then
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
for pattern in "${BLOCKED_PATTERNS[@]}"; do
|
|
39
|
+
if echo "$INPUT" | grep -iqE "$pattern"; then
|
|
40
|
+
echo "[SECURITY BLOCKED] Pattern matched: $pattern" >&2
|
|
41
|
+
echo "[SECURITY BLOCKED] Destructive command detected and blocked by Pantheon hook." >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
exit 0
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
writeFileSync,
|
|
14
14
|
} from 'node:fs'
|
|
15
15
|
import { homedir } from 'node:os'
|
|
16
|
-
import { join, resolve } from 'node:path'
|
|
16
|
+
import { basename, join, resolve } from 'node:path'
|
|
17
17
|
import {
|
|
18
18
|
bullet,
|
|
19
19
|
colors,
|
|
@@ -193,8 +193,9 @@ export async function installOpenCode(
|
|
|
193
193
|
const srcRouting = join(ROOT, 'src', 'routing.yml')
|
|
194
194
|
const dstRouting = join(target, 'routing.yml')
|
|
195
195
|
if (existsSync(srcRouting)) {
|
|
196
|
-
writeIfChanged(dstRouting, readFileSync(srcRouting, 'utf8'), dryRun)
|
|
197
|
-
stats.created++
|
|
196
|
+
const routingStatus = writeIfChanged(dstRouting, readFileSync(srcRouting, 'utf8'), dryRun)
|
|
197
|
+
if (routingStatus === 'created') stats.created++
|
|
198
|
+
else stats.skipped++
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
|
|
@@ -280,27 +281,12 @@ export async function installOpenCode(
|
|
|
280
281
|
|
|
281
282
|
// -----------------------------------------------------------------------
|
|
282
283
|
// -----------------------------------------------------------------------
|
|
283
|
-
// 2.7
|
|
284
|
+
// 2.7 subagent_depth is merged into opencode.json in the config section
|
|
285
|
+
// below (always runs) — see "Ensure critical top-level config sections".
|
|
286
|
+
// Previously this was a separate block gated on the config file already
|
|
287
|
+
// existing, so a FRESH install only gained subagent_depth on the SECOND
|
|
288
|
+
// init run (idempotence bug, issue #19).
|
|
284
289
|
// -----------------------------------------------------------------------
|
|
285
|
-
if (componentSet.has('agents')) {
|
|
286
|
-
const cfgPath = join(target, 'opencode.json')
|
|
287
|
-
if (existsSync(cfgPath)) {
|
|
288
|
-
try {
|
|
289
|
-
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'))
|
|
290
|
-
let changed = false
|
|
291
|
-
if (cfg.subagent_depth === undefined) {
|
|
292
|
-
cfg.subagent_depth = 2
|
|
293
|
-
changed = true
|
|
294
|
-
}
|
|
295
|
-
if (changed && !dryRun) {
|
|
296
|
-
writeFileSync(cfgPath, `${JSON.stringify(cfg, null, 2)}\n`)
|
|
297
|
-
success('subagent_depth: 2 added to opencode.json')
|
|
298
|
-
}
|
|
299
|
-
} catch (e) {
|
|
300
|
-
warning(`Could not update opencode.json: ${e.message}`)
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
290
|
|
|
305
291
|
// 2.8 Install TUI plugins (--components plugins)
|
|
306
292
|
// -----------------------------------------------------------------------
|
|
@@ -322,9 +308,16 @@ export async function installOpenCode(
|
|
|
322
308
|
const targetTuiConfigPath = isGlobal
|
|
323
309
|
? join(target, 'tui.json')
|
|
324
310
|
: join(target, '.opencode', 'tui.json')
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
311
|
+
// Hermetic TUI plugin registration: opencode's tui loader treats relative
|
|
312
|
+
// entries ("plugins/pantheon-tui") as npm/github package specs and fails
|
|
313
|
+
// with NpmInstallFailedError ("Repository not found"). Register the plugin
|
|
314
|
+
// by the ABSOLUTE path of the built dist inside the installed package
|
|
315
|
+
// (derived from ROOT — never hardcoded), mirroring the loader's
|
|
316
|
+
// absolute-path support (same pattern as the hooks plugin fix).
|
|
317
|
+
const tuiPluginRef = join(ROOT, 'src', 'plugins', 'tui', 'dist', 'tui.tsx')
|
|
318
|
+
// Clean up the old broken relative ref (and stale dist refs) on upgrade.
|
|
319
|
+
unregisterPlugin(targetTuiConfigPath, 'plugins/pantheon-tui', { dryRun })
|
|
320
|
+
const tuiStatus = registerPlugin(targetTuiConfigPath, tuiPluginRef, { dryRun })
|
|
328
321
|
if (tuiStatus === 'created') stats.created++
|
|
329
322
|
else stats.skipped++
|
|
330
323
|
|
|
@@ -347,7 +340,17 @@ export async function installOpenCode(
|
|
|
347
340
|
'pantheon_vision_server.py',
|
|
348
341
|
]
|
|
349
342
|
const srcScriptsDir = join(ROOT, 'scripts')
|
|
343
|
+
// Canonical MCP server sources live in src/mcp/. Map them explicitly so the
|
|
344
|
+
// sync always copies from the canonical source, never from the stale root
|
|
345
|
+
// scripts/ copies (which previously propagated bugs like the missing
|
|
346
|
+
// `import uuid` in mcp_persistence_server.py). scrub-secrets.py is not in
|
|
347
|
+
// src/mcp/ and intentionally falls back to join(ROOT, 'scripts').
|
|
350
348
|
const canonicalMcpScripts = {
|
|
349
|
+
'mcp_resources_server.py': join(ROOT, 'src', 'mcp', 'mcp_resources_server.py'),
|
|
350
|
+
'code_mode_server.py': join(ROOT, 'src', 'mcp', 'code_mode_server.py'),
|
|
351
|
+
'memory_mcp_server.py': join(ROOT, 'src', 'mcp', 'memory_mcp_server.py'),
|
|
352
|
+
'_pantheon_paths.py': join(ROOT, 'src', 'mcp', '_pantheon_paths.py'),
|
|
353
|
+
'mcp_persistence_server.py': join(ROOT, 'src', 'mcp', 'mcp_persistence_server.py'),
|
|
351
354
|
'pantheon_vision_server.py': join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py'),
|
|
352
355
|
}
|
|
353
356
|
const dstScriptsDir = join(runtimeTarget, 'scripts')
|
|
@@ -565,15 +568,41 @@ export async function installOpenCode(
|
|
|
565
568
|
if (!config.default_agent && pantheonConfig.default_agent) {
|
|
566
569
|
config.default_agent = pantheonConfig.default_agent
|
|
567
570
|
}
|
|
571
|
+
// Merge subagent_depth HERE (the config merge always runs, including on a
|
|
572
|
+
// fresh install) instead of a file-existence-gated block, so run #1 already
|
|
573
|
+
// produces the final config — run #2 must be a byte-identical no-op (#19).
|
|
574
|
+
if (config.subagent_depth === undefined) {
|
|
575
|
+
config.subagent_depth = 2
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// --------------------------------------------------------------------
|
|
579
|
+
// B.5.1 Hermetic plugin resolution (packaging fix)
|
|
580
|
+
// --------------------------------------------------------------------
|
|
581
|
+
// The packaged opencode.json may reference the pantheon-hooks plugin via a
|
|
582
|
+
// developer-machine absolute path (e.g. /home/<dev>/pantheon/src/plugins/...).
|
|
583
|
+
// Copying that verbatim into the user's config would break every global
|
|
584
|
+
// install on any other machine. Option A: rewrite the entry to the plugin
|
|
585
|
+
// file inside the INSTALLED package (derived from ROOT — never hardcoded),
|
|
586
|
+
// so both dev and global installs resolve to a path that actually exists.
|
|
587
|
+
// The source opencode.json keeps its dev path for local development; the
|
|
588
|
+
// transform happens at install/sync time only.
|
|
589
|
+
function resolveInstalledPlugin(plugin) {
|
|
590
|
+
const file = basename(plugin)
|
|
591
|
+
const packaged = join(ROOT, 'src', 'plugins', file)
|
|
592
|
+
return existsSync(packaged) ? packaged : plugin
|
|
593
|
+
}
|
|
568
594
|
|
|
569
595
|
if (!Array.isArray(config.plugin)) {
|
|
570
596
|
config.plugin = []
|
|
571
597
|
}
|
|
572
598
|
if (Array.isArray(pantheonConfig.plugin)) {
|
|
573
599
|
for (const plugin of pantheonConfig.plugin) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
600
|
+
const resolved = resolveInstalledPlugin(plugin)
|
|
601
|
+
const file = basename(resolved)
|
|
602
|
+
// Replace any pre-existing entry for the same plugin file (e.g. a stale
|
|
603
|
+
// dev-machine path from an earlier install) so upgrades stay hermetic.
|
|
604
|
+
config.plugin = config.plugin.filter((p) => basename(p) !== file)
|
|
605
|
+
config.plugin.push(resolved)
|
|
577
606
|
}
|
|
578
607
|
}
|
|
579
608
|
|
|
@@ -128,8 +128,10 @@ export function unregisterPlugin(tuiJsonPath, pluginId, { dryRun = false } = {})
|
|
|
128
128
|
if (!Array.isArray(tuiConfig.plugin)) {
|
|
129
129
|
tuiConfig.plugin = []
|
|
130
130
|
}
|
|
131
|
-
// Remove stale
|
|
132
|
-
|
|
131
|
+
// Remove stale refs: the bare relative id (openCode < 1.19 wrote
|
|
132
|
+
// "plugins/pantheon-tui", which its tui loader misreads as an npm/github
|
|
133
|
+
// spec → NpmInstallFailedError) and the old dist paths.
|
|
134
|
+
const staleRefs = [pluginId, `${pluginId}/dist/tui.tsx`, `${pluginId}/dist/tui.js`]
|
|
133
135
|
for (const stale of staleRefs) {
|
|
134
136
|
const idx = tuiConfig.plugin.indexOf(stale)
|
|
135
137
|
if (idx !== -1) tuiConfig.plugin.splice(idx, 1)
|