"""``harness: acp`` wrap (the generic Agent Client Protocol harness). Thin module exposing :func:`create_app` — the entry point the shared :mod:`omnigent.runtime.harnesses._runner` invokes after the parent process resolves ``"acp"`` (or ``"acp:"``) to this module. Wraps an :class:`omnigent.inner.acp_executor.AcpExecutor`, which drives *any* ACP agent command over the Agent Client Protocol — the vendor-agnostic counterpart to the ``goose`` / ``qwen`` wraps. Which agent runs is decided by the spawn-env the runner passes (see :func:`omnigent.runtime.workflow._build_acp_spawn_env`), which resolves the picked ``acp:`` to a user-configured command in the ``acp:`` config block. Auth is each agent's own (the user logs into their agent via its own CLI); Omnigent stores no credential. Tool approvals surface as web elicitation cards via ``session/request_permission`` (bridges the :class:`ExecutorAdapter` installs). Env vars read at startup: - ``HARNESS_ACP_COMMAND`` (required): the command to launch, e.g. ``"gemini --experimental-acp"``. Missing → a request-time error. - ``HARNESS_ACP_NAME``: display label for logs / elicitation cards. - ``HARNESS_ACP_MODEL``: optional model id (only sent when the agent is configured to accept one in ``session/new``). - ``HARNESS_ACP_SESSION_ID_MODE``: ``server`` (default) or ``client``. - ``HARNESS_ACP_SEND_MODEL``: ``"1"`` to send the model in ``session/new``. - ``HARNESS_ACP_OS_ENV``: JSON-encoded :class:`OSEnvSpec`. When unset, falls back to ``caller_process`` + ``sandbox=none``. """ from __future__ import annotations import json import logging import os from fastapi import FastAPI from omnigent.inner.acp_executor import AcpAgentConfig, AcpExecutor from omnigent.inner.datamodel import OSEnvSandboxSpec, OSEnvSpec from omnigent.inner.executor import Executor from omnigent.runtime.harnesses._executor_adapter import ExecutorAdapter _logger = logging.getLogger(__name__) _ENV_COMMAND = "HARNESS_ACP_COMMAND" _ENV_NAME = "HARNESS_ACP_NAME" _ENV_MODEL = "HARNESS_ACP_MODEL" _ENV_SESSION_ID_MODE = "HARNESS_ACP_SESSION_ID_MODE" _ENV_SEND_MODEL = "HARNESS_ACP_SEND_MODEL" _ENV_CWD = "HARNESS_ACP_CWD" _ENV_OS_ENV = "HARNESS_ACP_OS_ENV" def _resolve_os_env() -> OSEnvSpec: """Resolve the inner-executor :class:`OSEnvSpec` from env config. Decodes the JSON-encoded :data:`_ENV_OS_ENV`; falls back to ``caller_process`` + ``sandbox=none`` when the var is missing or malformed. """ raw = os.environ.get(_ENV_OS_ENV, "").strip() if raw: try: payload = json.loads(raw) except json.JSONDecodeError as exc: _logger.warning( "%s is not valid JSON (%s); falling back to default os_env", _ENV_OS_ENV, exc ) payload = None if isinstance(payload, dict): sandbox_payload = payload.get("sandbox") sandbox = ( OSEnvSandboxSpec(**sandbox_payload) if isinstance(sandbox_payload, dict) else None ) return OSEnvSpec( type=str(payload.get("type", "caller_process")), cwd=payload.get("cwd"), sandbox=sandbox, fork=bool(payload.get("fork", False)), ) return OSEnvSpec( type="caller_process", cwd=None, sandbox=OSEnvSandboxSpec(type="none"), fork=False, ) def _build_acp_executor() -> Executor: """Construct an :class:`AcpExecutor` from env-var config (lazily, on first turn).""" command = os.environ.get(_ENV_COMMAND, "").strip() if not command: raise RuntimeError( f"{_ENV_COMMAND} is not set — no ACP agent command configured. " "Add one via `omnigent setup` → configure harnesses → Custom ACP agent." ) name = os.environ.get(_ENV_NAME, "").strip() or "ACP agent" model = os.environ.get(_ENV_MODEL, "").strip() or None session_id_mode = os.environ.get(_ENV_SESSION_ID_MODE, "").strip() or "server" send_model = os.environ.get(_ENV_SEND_MODEL, "").strip() in ("1", "true", "yes") cwd = os.environ.get(_ENV_CWD) or os.environ.get("OMNIGENT_RUNNER_WORKSPACE") or None config = AcpAgentConfig( command=command, name=name, model=model, session_id_mode=session_id_mode, send_model_in_session_new=send_model, ) return AcpExecutor(config=config, cwd=cwd, os_env=_resolve_os_env()) def create_app() -> FastAPI: """Build the generic ACP harness's FastAPI app (required entry point). The wrapped :class:`AcpExecutor` is constructed lazily on the first turn, so a missing command / absent agent binary surfaces as a request-time error rather than an app-boot crash. """ label = os.environ.get(_ENV_NAME, "").strip() or "ACP agent" adapter = ExecutorAdapter(executor_factory=_build_acp_executor, harness_label=label) return adapter.build()