diff --git a/src/tgcodex/codex/events.py b/src/tgcodex/codex/events.py index 3f499e4..5570cdb 100644 --- a/src/tgcodex/codex/events.py +++ b/src/tgcodex/codex/events.py @@ -306,14 +306,24 @@ def parse_event_obj(obj: dict[str, Any]) -> list[CodexEvent]: # OpenAI tool-call format: surface exec_command requests and outputs as tool events. if t == "function_call": name = obj.get("name") - args_s = obj.get("arguments") - if name == "exec_command" and isinstance(args_s, str): - try: - args = json.loads(args_s) - except Exception: - args = None + args_raw = obj.get("arguments") + if name == "exec_command" and isinstance(args_raw, (str, dict)): + args = None + if isinstance(args_raw, str): + try: + args = json.loads(args_raw) + except Exception: + args = None + elif isinstance(args_raw, dict): + args = args_raw if isinstance(args, dict): cmd = args.get("cmd") + if isinstance(cmd, list): + argv = [str(c) for c in cmd] + try: + cmd = shlex.join(argv) + except Exception: + cmd = " ".join(argv) if isinstance(cmd, str) and cmd: call_id = obj.get("call_id") sandbox_perm = args.get("sandbox_permissions") diff --git a/tests/test_events.py b/tests/test_events.py index 9500669..bb36372 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -105,6 +105,21 @@ def test_function_call_exec_command_maps_to_tool_started(self) -> None: self.assertIsInstance(evs[0], ToolStarted) self.assertEqual(evs[0].command, "ls -la") + def test_function_call_exec_command_accepts_dict_arguments(self) -> None: + obj = { + "type": "response_item", + "payload": { + "type": "function_call", + "name": "exec_command", + "arguments": {"cmd": ["ls", "-la"]}, + "call_id": "call_abc123", + }, + } + evs = parse_event_obj(obj) + self.assertEqual(len(evs), 1) + self.assertIsInstance(evs[0], ToolStarted) + self.assertEqual(evs[0].command, "ls -la") + def test_function_call_exec_command_with_escalation_maps_to_exec_approval_request(self) -> None: obj = { "type": "response_item", @@ -122,6 +137,27 @@ def test_function_call_exec_command_with_escalation_maps_to_exec_approval_reques self.assertEqual(evs[0].reason, "delete foo") self.assertEqual(evs[0].call_id, "call_abc123") + def test_function_call_exec_command_with_dict_and_escalation_maps_to_exec_approval_request(self) -> None: + obj = { + "type": "response_item", + "payload": { + "type": "function_call", + "name": "exec_command", + "arguments": { + "cmd": ["rm", "-rf", "foo"], + "sandbox_permissions": "require_escalated", + "justification": "delete foo", + }, + "call_id": "call_abc123", + }, + } + evs = parse_event_obj(obj) + self.assertEqual(len(evs), 1) + self.assertIsInstance(evs[0], ExecApprovalRequest) + self.assertEqual(evs[0].command, "rm -rf foo") + self.assertEqual(evs[0].reason, "delete foo") + self.assertEqual(evs[0].call_id, "call_abc123") + def test_proto_envelope_exec_approval_request(self) -> None: obj = { "id": "0",