fix: observer fails closed on confirmation/permission prompts (issue #400)

This commit is contained in:
swarnika-cmd
2026-03-12 06:46:42 +05:30
parent da4db99c94
commit 8079d354d1

View File

@@ -33,6 +33,18 @@ resolve_python_cmd() {
return 0
fi
# FIX: Windows Git Bash — check known Python install paths directly
# because `command -v python` triggers the Microsoft Store alias instead
for win_py in \
"/c/Users/$USER/AppData/Local/Programs/Python/Python311/python" \
"/c/Users/$USER/AppData/Local/Programs/Python/Python312/python" \
"/c/Users/$USER/AppData/Local/Programs/Python/Python310/python"; do
if [ -x "$win_py" ]; then
printf '%s\n' "$win_py"
return 0
fi
done
if command -v python3 >/dev/null 2>&1; then
printf '%s\n' python3
return 0
@@ -93,11 +105,22 @@ CONFIG_DIR="${HOME}/.claude/homunculus"
OBSERVATIONS_FILE="${PROJECT_DIR}/observations.jsonl"
MAX_FILE_SIZE_MB=10
# Skip if disabled
# FIX: SENTINEL_FILE must be defined AFTER PROJECT_DIR is set by detect-project.sh
# Previously it was defined at the top before PROJECT_DIR existed, making it empty/broken
SENTINEL_FILE="${PROJECT_DIR}/.observer.lock"
# Skip if disabled globally
if [ -f "$CONFIG_DIR/disabled" ]; then
exit 0
fi
# FIX: Skip if a previous run already aborted due to confirmation/permission prompt
# This is the circuit-breaker — stops retrying after a non-interactive failure
if [ -f "$SENTINEL_FILE" ]; then
echo "[observe] Skipping: previous run aborted due to confirmation/permission prompt. Remove ${SENTINEL_FILE} to re-enable." >&2
exit 0
fi
# Auto-purge observation files older than 30 days (runs once per session)
PURGE_MARKER="${PROJECT_DIR}/.last-purge"
if [ ! -f "$PURGE_MARKER" ] || [ "$(find "$PURGE_MARKER" -mtime +1 2>/dev/null)" ]; then
@@ -190,46 +213,47 @@ if [ -f "$OBSERVATIONS_FILE" ]; then
fi
fi
# Build and write observation (now includes project context)
# Scrub common secret patterns from tool I/O before persisting
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# FIX: Detect confirmation/permission prompts in observer output and fail closed.
# A non-interactive background observer must never ask for user confirmation.
# If detected: log once, write sentinel to suppress all future retries, exit non-zero.
if echo "$PARSED" | grep -E -i -q "Can you confirm|requires permission|Awaiting|confirm I should proceed|once granted access|grant.*access"; then
echo "[observe] OBSERVER_ABORT: Confirmation or permission prompt detected in observer output. This observer run is non-actionable." >&2
echo "[observe] Writing sentinel to suppress retries: ${SENTINEL_FILE}" >&2
echo "$PARSED" > "$SENTINEL_FILE"
exit 2
fi
# Build and write observation (now includes project context)
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
export PROJECT_ID_ENV="$PROJECT_ID"
export PROJECT_NAME_ENV="$PROJECT_NAME"
export TIMESTAMP="$timestamp"
echo "$PARSED" | "$PYTHON_CMD" -c '
import json, sys, os, re
parsed = json.load(sys.stdin)
observation = {
"timestamp": os.environ["TIMESTAMP"],
"event": parsed["event"],
"tool": parsed["tool"],
"session": parsed["session"],
"project_id": os.environ.get("PROJECT_ID_ENV", "global"),
"project_name": os.environ.get("PROJECT_NAME_ENV", "global")
"timestamp": os.environ["TIMESTAMP"],
"event": parsed["event"],
"tool": parsed["tool"],
"session": parsed["session"],
"project_id": os.environ.get("PROJECT_ID_ENV", "global"),
"project_name": os.environ.get("PROJECT_NAME_ENV", "global")
}
# Scrub secrets: match common key=value, key: value, and key"value patterns
# Includes optional auth scheme (e.g., "Bearer", "Basic") before token
_SECRET_RE = re.compile(
r"(?i)(api[_-]?key|token|secret|password|authorization|credentials?|auth)"
r"""(["'"'"'\s:=]+)"""
r"([A-Za-z]+\s+)?"
r"([A-Za-z0-9_\-/.+=]{8,})"
r"(?i)(api[_-]?key|token|secret|password|authorization|credentials?|auth)"
r"""(["'"'"'\s:=]+)"""
r"([A-Za-z]+\s+)?"
r"([A-Za-z0-9_\-/.+=]{8,})"
)
def scrub(val):
if val is None:
return None
return _SECRET_RE.sub(lambda m: m.group(1) + m.group(2) + (m.group(3) or "") + "[REDACTED]", str(val))
if val is None:
return None
return _SECRET_RE.sub(lambda m: m.group(1) + m.group(2) + (m.group(3) or "") + "[REDACTED]", str(val))
if parsed["input"]:
observation["input"] = scrub(parsed["input"])
observation["input"] = scrub(parsed["input"])
if parsed["output"] is not None:
observation["output"] = scrub(parsed["output"])
observation["output"] = scrub(parsed["output"])
print(json.dumps(observation))
' >> "$OBSERVATIONS_FILE"
@@ -243,4 +267,4 @@ for pid_file in "${PROJECT_DIR}/.observer.pid" "${CONFIG_DIR}/.observer.pid"; do
fi
done
exit 0
exit 0