import sys import json import logging import os # Add project root to sys.path so we can import api_hook_client # This helps in cases where the script is run from different directories 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: # Fallback if the script is run from the project root directly, # or if the above path append didn't work for some reason. try: from api_hook_client import ApiHookClient except ImportError: # Use basic print for fatal errors if logging isn't set up yet print("FATAL: Failed to import ApiHookClient. Ensure it's in the Python path.", file=sys.stderr) sys.exit(1) # Exit if the core dependency cannot be imported def main(): # Setup basic logging to stderr. # Set level to DEBUG to capture all messages, including debug info. logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s', stream=sys.stderr) logging.debug("CLI Tool Bridge script started.") try: # 1. Read JSON from sys.stdin 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 # Initialize variables for tool name and arguments tool_name = None tool_args = {} # 2. Try to parse input in Gemini API format ('name', 'input') logging.debug("Attempting to parse input in Gemini API format ('name', 'input').") if 'name' in hook_input and hook_input['name'] is not None: tool_name = hook_input['name'] logging.debug(f"Found Gemini API format tool name: {tool_name}") if 'input' in hook_input and hook_input['input'] is not None: if isinstance(hook_input['input'], dict): tool_args = hook_input['input'] logging.debug(f"Found Gemini API format tool input: {tool_args}") else: logging.warning("Gemini API format 'input' is not a dictionary. Ignoring.") # 3. If Gemini format wasn't fully present, try the legacy format ('tool_name', 'tool_input') if tool_name is None: logging.debug("Gemini API format not fully detected. Falling back to legacy format ('tool_name', 'tool_input').") tool_name = hook_input.get('tool_name') if tool_name: logging.debug(f"Found legacy format tool name: {tool_name}") tool_input_legacy = hook_input.get('tool_input') if tool_input_legacy is not None: if isinstance(tool_input_legacy, dict): tool_args = tool_input_legacy logging.debug(f"Found legacy format tool input: {tool_args}") else: logging.warning("Legacy format 'tool_input' is not a dictionary. Ignoring.") # Final checks on resolved tool_name and tool_args if tool_name is None: logging.error("Could not determine tool name from input.") print(json.dumps({ "decision": "deny", "reason": "Could not determine tool name from input. Expected 'name' or 'tool_name'." })) return if not isinstance(tool_args, dict): logging.error(f"Resolved tool_args is not a dictionary: {tool_args}") print(json.dumps({ "decision": "deny", "reason": "Resolved tool arguments are not in a valid dictionary format." })) return logging.debug(f"Resolved tool_name: '{tool_name}', tool_args: {tool_args}") # 4. Check context — if not running via Manual Slop, we pass through (allow) # This prevents the hook from affecting normal CLI usage. hook_context = os.environ.get("GEMINI_CLI_HOOK_CONTEXT") logging.debug(f"Checking GEMINI_CLI_HOOK_CONTEXT: '{hook_context}'") if hook_context != "manual_slop" and hook_context != "mma_headless": logging.debug(f"GEMINI_CLI_HOOK_CONTEXT is '{hook_context}', NOT 'manual_slop' or 'mma_headless'. Allowing execution without confirmation.") print(json.dumps({ "decision": "allow", "reason": f"Non-programmatic usage (GEMINI_CLI_HOOK_CONTEXT={hook_context})." })) return if hook_context == "mma_headless": logging.debug(f"GEMINI_CLI_HOOK_CONTEXT is 'mma_headless'. Allowing execution for sub-agent.") print(json.dumps({ "decision": "allow", "reason": "Sub-agent headless mode (MMA)." })) return # 5. Use 'ApiHookClient' (assuming GUI is on http://127.0.0.1:8999) logging.debug("GEMINI_CLI_HOOK_CONTEXT is 'manual_slop'. Proceeding with API Hook Client.") client = ApiHookClient(base_url="http://127.0.0.1:8999") try: # 6. Request confirmation # This is a blocking call that waits for the user in the GUI logging.debug(f"Requesting confirmation for tool '{tool_name}' with args: {tool_args}") response = client.request_confirmation(tool_name, tool_args) if response and response.get('approved') is True: # 7. Print 'allow' decision logging.debug("User approved tool execution.") print(json.dumps({"decision": "allow"})) else: # 8. Print 'deny' decision 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: # 9. Handle cases where hook server is not reachable or other API errors # If we ARE in manual_slop context but can't reach the server, we should DENY # because the user expects to be in control. logging.error(f"API Hook Client error: {str(e)}", exc_info=True) print(json.dumps({ "decision": "deny", "reason": f"Manual Slop hook server unreachable or API error: {str(e)}" })) except Exception as e: # Fallback for unexpected errors during initial processing (e.g., stdin read) logging.error(f"An unexpected error occurred in the main bridge logic: {str(e)}", exc_info=True) print(json.dumps({ "decision": "deny", "reason": f"Internal bridge error: {str(e)}" })) if __name__ == "__main__": main()