Files
manual_slop/scripts/claude_tool_bridge.py

78 lines
3.5 KiB
Python

import sys
import json
import logging
import os
# Add project root to sys.path so we can import api_hook_client
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if project_root not in sys.path:
sys.path.append(project_root)
try:
from api_hook_client import ApiHookClient
except ImportError:
print("FATAL: Failed to import ApiHookClient. Ensure it's in the Python path.", file=sys.stderr)
sys.exit(1)
def main() -> None:
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s', stream=sys.stderr)
logging.debug("Claude Tool Bridge script started.")
try:
input_data = sys.stdin.read()
if not input_data:
logging.debug("No input received from stdin. Exiting gracefully.")
return
logging.debug(f"Received raw input data: {input_data}")
try:
hook_input = json.loads(input_data)
except json.JSONDecodeError:
logging.error("Failed to decode JSON from stdin.")
print(json.dumps({"decision": "deny", "reason": "Invalid JSON received from stdin."}))
return
# Claude Code PreToolUse hook format: tool_name + tool_input
tool_name = hook_input.get('tool_name')
tool_input = hook_input.get('tool_input', {})
if tool_name is None:
logging.error("Could not determine tool name from input. Expected 'tool_name'.")
print(json.dumps({"decision": "deny", "reason": "Missing 'tool_name' in hook input."}))
return
if not isinstance(tool_input, dict):
logging.warning(f"tool_input is not a dict: {tool_input}. Treating as empty.")
tool_input = {}
logging.debug(f"Resolved tool_name: '{tool_name}', tool_input: {tool_input}")
# Check context — if not running via Manual Slop, pass through
hook_context = os.environ.get("CLAUDE_CLI_HOOK_CONTEXT")
logging.debug(f"Checking CLAUDE_CLI_HOOK_CONTEXT: '{hook_context}'")
if hook_context == 'mma_headless':
# Sub-agents in headless MMA mode: auto-allow all tools
logging.debug("CLAUDE_CLI_HOOK_CONTEXT is 'mma_headless'. Allowing for sub-agent.")
print(json.dumps({"decision": "allow", "reason": "Sub-agent headless mode (MMA)."}))
return
if hook_context != 'manual_slop':
# Not a programmatic Manual Slop session — allow through silently
logging.debug(f"CLAUDE_CLI_HOOK_CONTEXT is '{hook_context}', not 'manual_slop'. Allowing.")
print(json.dumps({"decision": "allow", "reason": f"Non-programmatic usage (CLAUDE_CLI_HOOK_CONTEXT={hook_context})."}))
return
# manual_slop context: route to GUI for approval
logging.debug("CLAUDE_CLI_HOOK_CONTEXT is 'manual_slop'. Routing to API Hook Client.")
client = ApiHookClient(base_url="http://127.0.0.1:8999")
try:
logging.debug(f"Requesting confirmation for tool '{tool_name}' with args: {tool_input}")
response = client.request_confirmation(tool_name, tool_input)
if response and response.get('approved') is True:
logging.debug("User approved tool execution.")
print(json.dumps({"decision": "allow"}))
else:
reason = response.get('reason', 'User rejected tool execution in GUI.') if response else 'No response from GUI.'
logging.debug(f"User denied tool execution. Reason: {reason}")
print(json.dumps({"decision": "deny", "reason": reason}))
except Exception as e:
logging.error(f"API Hook Client error: {str(e)}", exc_info=True)
print(json.dumps({"decision": "deny", "reason": f"Manual Slop hook server unreachable: {str(e)}"}))
except Exception as e:
logging.error(f"Unexpected error in bridge: {str(e)}", exc_info=True)
print(json.dumps({"decision": "deny", "reason": f"Internal bridge error: {str(e)}"}))
if __name__ == "__main__":
main()