78 lines
3.5 KiB
Python
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()
|