from __future__ import annotations import json import subprocess from hashlib import sha1 from .base import ProviderResult, TaskProvider class CodexCLITaskProvider(TaskProvider): name = "codex_cli" run_in_worker = True def _timeout(self, config: dict) -> int: try: return max(1, int(config.get("timeout_seconds") or 60)) except Exception: return 60 def _command(self, config: dict) -> str: return str(config.get("command") or "codex").strip() or "codex" def _workspace(self, config: dict) -> str: return str(config.get("workspace_root") or "").strip() def _profile(self, config: dict) -> str: return str(config.get("default_profile") or "").strip() def _is_task_sync_contract_mismatch(self, stderr: str) -> bool: text = str(stderr or "").lower() if "unexpected argument '--op'" in text: return True if "unexpected argument 'create'" in text and "usage: codex" in text: return True if "unexpected argument 'append_update'" in text and "usage: codex" in text: return True if "unexpected argument 'mark_complete'" in text and "usage: codex" in text: return True if "unexpected argument 'link_task'" in text and "usage: codex" in text: return True if "unrecognized subcommand 'create'" in text and "usage: codex" in text: return True if "unrecognized subcommand 'append_update'" in text and "usage: codex" in text: return True if "unrecognized subcommand 'mark_complete'" in text and "usage: codex" in text: return True return False def _builtin_stub_result(self, op: str, payload: dict, stderr: str) -> ProviderResult: mode = str(payload.get("mode") or "default").strip().lower() external_key = ( str(payload.get("external_key") or "").strip() or str(payload.get("task_id") or "").strip() ) if mode == "approval_response": return ProviderResult( ok=True, external_key=external_key, payload={ "op": op, "status": "ok", "summary": "approval acknowledged; resumed by builtin codex stub", "requires_approval": False, "output": "", "fallback_mode": "builtin_task_sync_stub", "fallback_reason": str(stderr or "")[:4000], }, ) task_id = str(payload.get("task_id") or "").strip() key_basis = f"{op}:{task_id}:{payload.get('trigger_message_id') or payload.get('origin_message_id') or ''}" approval_key = sha1(key_basis.encode("utf-8")).hexdigest()[:12] summary = "Codex approval required (builtin stub fallback)" return ProviderResult( ok=True, external_key=external_key, payload={ "op": op, "status": "requires_approval", "requires_approval": True, "summary": summary, "approval_key": approval_key, "permission_request": { "summary": summary, "requested_permissions": ["workspace_write"], }, "resume_payload": { "task_id": task_id, "op": op, }, "fallback_mode": "builtin_task_sync_stub", "fallback_reason": str(stderr or "")[:4000], }, ) def _run(self, config: dict, op: str, payload: dict) -> ProviderResult: base_cmd = [self._command(config), "task-sync"] workspace = self._workspace(config) profile = self._profile(config) command_timeout = self._timeout(config) data = json.dumps(dict(payload or {}), separators=(",", ":")) common_args: list[str] = [] if workspace: common_args.extend(["--workspace", workspace]) if profile: common_args.extend(["--profile", profile]) primary_cmd = [*base_cmd, "--op", str(op), *common_args, "--payload-json", data] fallback_cmd = [*base_cmd, str(op), *common_args, "--payload-json", data] try: completed = subprocess.run( primary_cmd, capture_output=True, text=True, timeout=command_timeout, check=False, cwd=workspace if workspace else None, ) stderr_probe = str(completed.stderr or "").lower() if completed.returncode != 0 and "unexpected argument '--op'" in stderr_probe: completed = subprocess.run( fallback_cmd, capture_output=True, text=True, timeout=command_timeout, check=False, cwd=workspace if workspace else None, ) except subprocess.TimeoutExpired: return ProviderResult( ok=False, error=f"codex_cli_timeout_{command_timeout}s", payload={"op": op, "timeout_seconds": command_timeout}, ) except Exception as exc: return ProviderResult(ok=False, error=f"codex_cli_exec_error:{exc}", payload={"op": op}) stdout = str(completed.stdout or "").strip() stderr = str(completed.stderr or "").strip() parsed = {} if stdout: try: parsed = json.loads(stdout) if not isinstance(parsed, dict): parsed = {"raw_stdout": stdout} except Exception: parsed = {"raw_stdout": stdout} parsed_status = str(parsed.get("status") or "").strip().lower() permission_request = parsed.get("permission_request") requires_approval = bool( parsed.get("requires_approval") or parsed_status in {"requires_approval", "waiting_approval"} or permission_request ) ext = ( str(parsed.get("external_key") or "").strip() or str(parsed.get("task_id") or "").strip() or str(payload.get("external_key") or "").strip() ) ok = completed.returncode == 0 out_payload = { "op": op, "returncode": int(completed.returncode), "stdout": stdout[:4000], "stderr": stderr[:4000], "parsed_status": parsed_status, "requires_approval": requires_approval, } out_payload.update(parsed) if (not ok) and self._is_task_sync_contract_mismatch(stderr): return self._builtin_stub_result(op, dict(payload or {}), stderr) return ProviderResult(ok=ok, external_key=ext, error=("" if ok else stderr[:4000]), payload=out_payload) def healthcheck(self, config: dict) -> ProviderResult: command = self._command(config) try: completed = subprocess.run( [command, "--version"], capture_output=True, text=True, timeout=max(1, min(20, self._timeout(config))), check=False, ) except Exception as exc: return ProviderResult(ok=False, error=f"codex_cli_unavailable:{exc}") return ProviderResult( ok=(completed.returncode == 0), payload={ "returncode": int(completed.returncode), "stdout": str(completed.stdout or "").strip()[:1000], "stderr": str(completed.stderr or "").strip()[:1000], }, error=("" if completed.returncode == 0 else str(completed.stderr or "").strip()[:1000]), ) def create_task(self, config: dict, payload: dict) -> ProviderResult: return self._run(config, "create", payload) def append_update(self, config: dict, payload: dict) -> ProviderResult: return self._run(config, "append_update", payload) def mark_complete(self, config: dict, payload: dict) -> ProviderResult: return self._run(config, "mark_complete", payload) def link_task(self, config: dict, payload: dict) -> ProviderResult: return self._run(config, "link_task", payload)