5b42a113c3
* fix(harnesses): surface Hermes in the web harness picker Hermes is a valid, installable harness (present in valid_harnesses and harness_modules with declared capabilities) but had no harness_labels entry, so harness_catalog() -- which iterates the labels -- dropped it from GET /v1/harnesses. The web picker therefore never listed Hermes even though "omnigent setup" (which hardcodes the row) shows it ready. Add the label, matching the subprocess-harness convention of codex/cursor/pi. The frontend already maps the hermes harness to HermesIcon, so no frontend change is needed. Closes #1939 Co-authored-by: Isaac Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com> * fix(harnesses): thread the spawn env for the hermes picker row Adding hermes to `harness_labels` makes it selectable in the web picker, but hermes had no spawn-env builder and no `model_env_keys` entry, so `_build_spawn_env_from_spec` returned None for it and the subprocess started with no per-session config at all. The wrap then applied its own defaults, and three picker choices became silent no-ops: - a selected sandbox fell back to the wrap's `caller_process` + `sandbox=none`, so a session the UI showed as sandboxed ran unconfined; - the session workspace fell back to the runner-wide `OMNIGENT_RUNNER_WORKSPACE` instead of the folder the user picked; - `/model` was rejected up front, since `harness_supports_model_override` derives from `model_env_keys`. Add `_build_hermes_spawn_env`, modelled on the kimi builder: hermes owns its file-based auth (`hermes setup` / `hermes model`, credentials under its `HERMES_HOME`), so there is no gateway/provider surface to configure and the builder threads only model, cwd, skills filter, and the serialized `os_env`. Unlike kimi it does emit `HARNESS_HERMES_SKILLS_FILTER`, which the executor turns into its `-s` / `--ignore-rules` argv. `HARNESS_HERMES_BUNDLE_DIR` stays unset: it is reserved in the wrap with no `hermes chat` flag to carry it, so emitting it would set a var the executor cannot pass on. Register the builder on the `hermes` arm of the runner dispatch chain, matching the eleven sibling builtins, and add the model env key so `/model` reaches the subprocess. Tests: hermes joins the shared parametrized cwd and `OMNIGENT_*_PATH` suites, gains four builder tests beside its kimi peer, a dispatch-chain guard (having a builder does not prove the chain reaches it), and a guard that the picker row's model plumbing exists. Each fails on the unfixed tree for its own reason. Co-authored-by: Isaac Signed-off-by: Dhruv Gupta <dhruv0811@gmail.com> --------- Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com> Signed-off-by: Dhruv Gupta <dhruv0811@gmail.com> Co-authored-by: Dhruv Gupta <dhruv0811@gmail.com>
1207 lines
41 KiB
Python
1207 lines
41 KiB
Python
"""Dynamic harness contribution registry.
|
|
|
|
Core Omnigent contributes the built-in harnesses directly. Optional community
|
|
packages contribute additional harnesses through the
|
|
``omnigent.community.harness`` entry point group.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import importlib.metadata
|
|
import logging
|
|
from collections.abc import Callable, Iterable, Mapping
|
|
from dataclasses import dataclass, field
|
|
from typing import TypeVar, cast
|
|
|
|
from omnigent._wrapper_labels import (
|
|
ANTIGRAVITY_NATIVE_WRAPPER_VALUE,
|
|
CLAUDE_NATIVE_WRAPPER_VALUE,
|
|
CODEX_NATIVE_WRAPPER_VALUE,
|
|
CURSOR_NATIVE_WRAPPER_VALUE,
|
|
GOOSE_NATIVE_WRAPPER_VALUE,
|
|
HERMES_NATIVE_WRAPPER_VALUE,
|
|
KIMI_NATIVE_WRAPPER_VALUE,
|
|
KIRO_NATIVE_WRAPPER_VALUE,
|
|
OPENCODE_NATIVE_WRAPPER_VALUE,
|
|
PI_NATIVE_WRAPPER_VALUE,
|
|
QWEN_NATIVE_WRAPPER_VALUE,
|
|
UI_MODE_LABEL_KEY,
|
|
UI_MODE_TERMINAL_VALUE,
|
|
WRAPPER_LABEL_KEY,
|
|
)
|
|
from omnigent.acp_cli_harnesses import ACP_CLI_HARNESSES
|
|
from omnigent.harness_capabilities import (
|
|
AuthModel,
|
|
EffortFamily,
|
|
Elicitation,
|
|
ForkHistory,
|
|
HarnessCapabilities,
|
|
IntegrationMode,
|
|
ModelFamily,
|
|
Resume,
|
|
)
|
|
from omnigent.harness_install_spec import HarnessInstallSpec
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
COMMUNITY_ENTRY_POINT_GROUP = "omnigent.community.harness"
|
|
COMMUNITY_MODULE_PREFIX = "omnigent.community.harness."
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class NativeCodingAgent:
|
|
"""Stable wire metadata for a native coding-agent TUI."""
|
|
|
|
key: str
|
|
display_name: str
|
|
agent_name: str
|
|
harness: str
|
|
wrapper_label: str
|
|
terminal_name: str
|
|
subagent_wrapper_label: str | None = None
|
|
|
|
@property
|
|
def presentation_labels(self) -> dict[str, str]:
|
|
"""Return labels that make sessions render terminal-first."""
|
|
return {
|
|
UI_MODE_LABEL_KEY: UI_MODE_TERMINAL_VALUE,
|
|
WRAPPER_LABEL_KEY: self.wrapper_label,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BackgroundTitleGeneratorSpec:
|
|
"""Lazy background-title generator registration for one harness."""
|
|
|
|
generator: str
|
|
resolver_harness: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class NativeHarnessProvider:
|
|
"""Import paths for a native harness's lifecycle hooks.
|
|
|
|
``NativeCodingAgent`` is pure identity data; behavior lives here as a
|
|
sibling row keyed by the same ``key``. Every value is a dotted import path
|
|
(``module:attr`` or ``module.attr``) resolved lazily at dispatch time via
|
|
:mod:`omnigent.native_dispatch`, so building the registry never imports the
|
|
runner / CLI / native-harness stack. Optional hooks are ``None`` when the
|
|
behavior is not yet a module-level function the resolver can reach (e.g.
|
|
interrupt/stop handlers that are still runner closures, or the inline
|
|
spawn-env dispatch); those hubs migrate onto the seam in later phases.
|
|
"""
|
|
|
|
key: str # matches NativeCodingAgent.key
|
|
run_native: str # CLI + resume launch entry point
|
|
auto_create_terminal: str # runner terminal builder
|
|
spawn_env_builder: str | None = None
|
|
# Session-label key carrying this harness's bridge id, when its spawn-env
|
|
# builder takes a ``bridge_id=`` kwarg resolved from session labels
|
|
# (codex/opencode/antigravity). ``None`` for bare builders and for harnesses
|
|
# whose bridge id resolves through a different path (e.g. claude).
|
|
bridge_id_label_key: str | None = None
|
|
interrupt_handler: str | None = None
|
|
stop_handler: str | None = None
|
|
materialize_agent_spec: str | None = None # built-in agent seeding
|
|
bridge_dir: str | None = None # cost-popup bridge-dir lookup
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class HarnessContribution:
|
|
"""One package's harness registry contribution."""
|
|
|
|
name: str
|
|
valid_harnesses: frozenset[str] = frozenset()
|
|
harness_modules: dict[str, str] = field(default_factory=dict)
|
|
aliases: dict[str, str] = field(default_factory=dict)
|
|
native_harnesses: frozenset[str] = frozenset()
|
|
native_agents: tuple[NativeCodingAgent, ...] = ()
|
|
native_providers: tuple[NativeHarnessProvider, ...] = ()
|
|
install_specs: dict[str, HarnessInstallSpec] = field(default_factory=dict)
|
|
harness_install_keys: dict[str, str] = field(default_factory=dict)
|
|
model_env_keys: dict[str, str] = field(default_factory=dict)
|
|
spawn_env_builders: dict[str, str] = field(default_factory=dict)
|
|
background_title_generators: dict[str, BackgroundTitleGeneratorSpec] = field(
|
|
default_factory=dict
|
|
)
|
|
missing_install_package: dict[str, str] = field(default_factory=dict)
|
|
harness_labels: dict[str, str] = field(default_factory=dict)
|
|
# Declared feature set per harness id ("what can this harness do?"). Sparse
|
|
# is allowed — a harness with no entry simply has no declared capabilities.
|
|
capabilities: dict[str, HarnessCapabilities] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class HarnessPluginState:
|
|
"""Merged harness registry plus non-fatal plugin load errors."""
|
|
|
|
contributions: tuple[HarnessContribution, ...]
|
|
load_errors: dict[str, str]
|
|
|
|
|
|
CLAUDE_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="claude",
|
|
display_name="Claude",
|
|
agent_name="claude-native-ui",
|
|
harness="claude-native",
|
|
wrapper_label=CLAUDE_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="claude",
|
|
subagent_wrapper_label="claude-code-native-ui-subagent",
|
|
)
|
|
|
|
CODEX_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="codex",
|
|
display_name="Codex",
|
|
agent_name="codex-native-ui",
|
|
harness="codex-native",
|
|
wrapper_label=CODEX_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="codex",
|
|
subagent_wrapper_label="codex-native-ui-subagent",
|
|
)
|
|
|
|
PI_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="pi",
|
|
display_name="Pi",
|
|
agent_name="pi-native-ui",
|
|
harness="pi-native",
|
|
wrapper_label=PI_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="pi",
|
|
)
|
|
|
|
OPENCODE_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="opencode",
|
|
display_name="OpenCode",
|
|
agent_name="opencode-native-ui",
|
|
harness="opencode-native",
|
|
wrapper_label=OPENCODE_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="opencode",
|
|
subagent_wrapper_label="opencode-native-ui-subagent",
|
|
)
|
|
|
|
CURSOR_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="cursor",
|
|
display_name="Cursor",
|
|
agent_name="cursor-native-ui",
|
|
harness="cursor-native",
|
|
wrapper_label=CURSOR_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="cursor",
|
|
)
|
|
|
|
KIRO_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="kiro",
|
|
display_name="Kiro",
|
|
agent_name="kiro-native-ui",
|
|
harness="kiro-native",
|
|
wrapper_label=KIRO_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="kiro",
|
|
)
|
|
|
|
GOOSE_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="goose",
|
|
display_name="Goose",
|
|
agent_name="goose-native-ui",
|
|
harness="goose-native",
|
|
wrapper_label=GOOSE_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="goose",
|
|
)
|
|
|
|
ANTIGRAVITY_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="antigravity",
|
|
display_name="Antigravity",
|
|
agent_name="antigravity-native-ui",
|
|
harness="antigravity-native",
|
|
wrapper_label=ANTIGRAVITY_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="antigravity",
|
|
)
|
|
|
|
QWEN_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="qwen",
|
|
display_name="Qwen Code",
|
|
agent_name="qwen-native-ui",
|
|
harness="qwen-native",
|
|
wrapper_label=QWEN_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="qwen",
|
|
)
|
|
|
|
KIMI_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="kimi",
|
|
display_name="Kimi",
|
|
agent_name="kimi-native-ui",
|
|
harness="kimi-native",
|
|
wrapper_label=KIMI_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="kimi",
|
|
)
|
|
|
|
HERMES_NATIVE_CODING_AGENT = NativeCodingAgent(
|
|
key="hermes",
|
|
display_name="Hermes",
|
|
agent_name="hermes-native-ui",
|
|
harness="hermes-native",
|
|
wrapper_label=HERMES_NATIVE_WRAPPER_VALUE,
|
|
terminal_name="hermes",
|
|
)
|
|
|
|
|
|
# Native harnesses whose spawn-env builder takes a ``bridge_id=`` resolved from
|
|
# a session label. Their label key follows the uniform
|
|
# ``omnigent.<key>_native.bridge_id`` pattern (pinned against the real bridge
|
|
# constants in tests/test_harness_plugins.py). Claude also carries a bridge id
|
|
# but resolves it through a runner helper with a server-side fallback, so it is
|
|
# handled as a spawn-env special case rather than a plain label read.
|
|
_BRIDGE_ID_LABEL_HARNESSES: frozenset[str] = frozenset({"codex", "opencode", "antigravity"})
|
|
|
|
|
|
def _builtin_native_provider(key: str) -> NativeHarnessProvider:
|
|
"""Build a built-in provider row from the ``omnigent.<key>_native`` module.
|
|
|
|
The built-in native harnesses follow a uniform module layout: each exports
|
|
``run_<key>_native`` (CLI + resume launch) and ``_materialize_<key>_agent_spec``
|
|
(agent seeding), exposes a ``_launch_<key>`` terminal adapter in
|
|
``omnigent.runner.native``, and exposes ``build_<key>_native_spawn_env`` in
|
|
``omnigent.<key>_native_bridge``. The remaining hooks (interrupt, stop,
|
|
bridge-dir) are still runner-local closures / inline dispatch, so they stay
|
|
``None`` until those hubs migrate onto the seam.
|
|
"""
|
|
module = f"omnigent.{key}_native"
|
|
return NativeHarnessProvider(
|
|
key=key,
|
|
run_native=f"{module}:run_{key}_native",
|
|
auto_create_terminal=f"omnigent.runner.native:_launch_{key}",
|
|
spawn_env_builder=f"{module}_bridge:build_{key}_native_spawn_env",
|
|
bridge_id_label_key=(f"{module}.bridge_id" if key in _BRIDGE_ID_LABEL_HARNESSES else None),
|
|
materialize_agent_spec=f"{module}:_materialize_{key}_agent_spec",
|
|
)
|
|
|
|
|
|
# Behavior side-channel for the built-in native agents. One row per
|
|
# NativeCodingAgent above, keyed by the same ``key``; resolved lazily so this
|
|
# module stays import-light. See designs/harness-modular-registry-proposal.md
|
|
# (Phase 1). Populated uniformly because every built-in native harness shares
|
|
# the omnigent.<key>_native module layout.
|
|
_BUILTIN_NATIVE_PROVIDERS: tuple[NativeHarnessProvider, ...] = tuple(
|
|
_builtin_native_provider(agent.key)
|
|
for agent in (
|
|
CLAUDE_NATIVE_CODING_AGENT,
|
|
CODEX_NATIVE_CODING_AGENT,
|
|
PI_NATIVE_CODING_AGENT,
|
|
OPENCODE_NATIVE_CODING_AGENT,
|
|
CURSOR_NATIVE_CODING_AGENT,
|
|
KIRO_NATIVE_CODING_AGENT,
|
|
GOOSE_NATIVE_CODING_AGENT,
|
|
ANTIGRAVITY_NATIVE_CODING_AGENT,
|
|
QWEN_NATIVE_CODING_AGENT,
|
|
KIMI_NATIVE_CODING_AGENT,
|
|
HERMES_NATIVE_CODING_AGENT,
|
|
)
|
|
)
|
|
|
|
|
|
# Declared capabilities for the built-in harnesses. Each value is backed by the
|
|
# module that implements it; the derivable axes (model_family, subagents) are
|
|
# asserted against their source in tests/test_harness_capabilities.py so the
|
|
# table cannot silently drift. See designs/harness-modular-registry-proposal.md.
|
|
_C = HarnessCapabilities
|
|
_IM = IntegrationMode
|
|
_EL = Elicitation
|
|
_RS = Resume
|
|
_EF = EffortFamily
|
|
_MF = ModelFamily
|
|
_AU = AuthModel
|
|
_FH = ForkHistory
|
|
|
|
# Bench shell-tool provocation prompts (moved off the bench's hardcoded
|
|
# _NATIVE_TOOL_PROVOCATION table): the generic variant, and a Bash-specific one
|
|
# for harnesses whose exec tool is literally "Bash". Both keep the
|
|
# "omnigent-bench-ok" placeholder the bench token-swaps per allow/deny probe.
|
|
_SHELL_PROMPT = "Use your shell/terminal tool to run this exact command: echo omnigent-bench-ok"
|
|
_BASH_PROMPT = "Use the Bash tool to run this exact command: echo omnigent-bench-ok"
|
|
|
|
# Trailing two bools are (interrupt, streaming). Only the four P0 SDK harnesses
|
|
# (claude-sdk, codex, pi, openai-agents) have these verified live by the harness
|
|
# bench today; the rest are declared best-effort by integration mode and will be
|
|
# reconciled against the bench's interrupt/streaming probes as coverage expands.
|
|
_BUILTIN_CAPABILITIES: dict[str, HarnessCapabilities] = {
|
|
# Native-CLI harnesses (wrap a resident vendor TUI/server).
|
|
"claude-native": _C(
|
|
_IM.NATIVE_TUI,
|
|
_EL.HOOK,
|
|
_RS.WARM_REATTACH,
|
|
_EF.ANTHROPIC,
|
|
_MF.CLAUDE,
|
|
_AU.OMNIGENT_CREDENTIAL,
|
|
subagents=True,
|
|
interrupt=True,
|
|
streaming=True,
|
|
fork_history=_FH.REBUILD,
|
|
shell_tool_name="Bash",
|
|
shell_tool_prompt=_BASH_PROMPT,
|
|
),
|
|
"codex-native": _C(
|
|
_IM.NATIVE_TUI,
|
|
_EL.JSONRPC,
|
|
_RS.WARM_REATTACH,
|
|
_EF.OPENAI,
|
|
_MF.GPT,
|
|
_AU.OMNIGENT_CREDENTIAL,
|
|
subagents=True,
|
|
interrupt=True,
|
|
streaming=True,
|
|
fork_history=_FH.REBUILD,
|
|
shell_tool_name="shell",
|
|
shell_tool_prompt=_SHELL_PROMPT,
|
|
),
|
|
# streaming is declared True unless a live bench run proves a harness does
|
|
# NOT emit token-level deltas. Only kiro-native is so proven (0 deltas over
|
|
# a full SSE capture); a static "forwarder posts no external_output_text_delta"
|
|
# grep is NOT sufficient — pi-native has no such delta-posting forwarder yet
|
|
# streams 7 deltas live (by what path was not traced), so the grep-based
|
|
# flip was wrong for it. The rest stay True until live-verified.
|
|
"pi-native": _C(
|
|
_IM.NATIVE_TUI,
|
|
_EL.NONE,
|
|
_RS.WARM_REATTACH,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.SESSION_SCOPED_CONFIG,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
fork_history=_FH.REBUILD,
|
|
shell_tool_name="Bash",
|
|
shell_tool_prompt=_BASH_PROMPT,
|
|
),
|
|
# streaming=False is LIVE-VERIFIED: a bench run observed 0 text deltas.
|
|
"cursor-native": _C(
|
|
_IM.NATIVE_TUI,
|
|
_EL.APPROVAL_MIRROR,
|
|
_RS.WARM_REATTACH,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=False,
|
|
fork_history=_FH.PREAMBLE,
|
|
# No shell-tool provocation: cursor-native was intentionally absent from
|
|
# the bench's table (its tool probe is skipped), so leave shell_tool_* None.
|
|
),
|
|
# kiro_native_permissions.py: "TUI ACP recorder -> web elicitation".
|
|
# streaming=False is LIVE-VERIFIED: a full SSE capture recorded 0 text
|
|
# deltas; the whole reply arrives as one response.output_item.done.
|
|
"kiro-native": _C(
|
|
_IM.NATIVE_TUI,
|
|
_EL.APPROVAL_MIRROR,
|
|
_RS.WARM_REATTACH,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=False,
|
|
fork_history=_FH.NONE,
|
|
shell_tool_name="shell",
|
|
shell_tool_prompt=_SHELL_PROMPT,
|
|
),
|
|
"antigravity-native": _C(
|
|
_IM.NATIVE_TUI,
|
|
_EL.NONE,
|
|
_RS.WARM_REATTACH,
|
|
_EF.GEMINI,
|
|
_MF.GEMINI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
fork_history=_FH.NONE,
|
|
shell_tool_name="run_command",
|
|
shell_tool_prompt=_SHELL_PROMPT,
|
|
),
|
|
"goose-native": _C(
|
|
_IM.NATIVE_TUI,
|
|
_EL.APPROVAL_MIRROR,
|
|
_RS.WARM_REATTACH,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
fork_history=_FH.NONE,
|
|
shell_tool_name="developer__shell",
|
|
shell_tool_prompt=_SHELL_PROMPT,
|
|
),
|
|
# streaming=False is LIVE-VERIFIED: a bench run observed 0 text deltas.
|
|
"qwen-native": _C(
|
|
_IM.NATIVE_TUI,
|
|
_EL.APPROVAL_MIRROR,
|
|
_RS.WARM_REATTACH,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=False,
|
|
fork_history=_FH.REBUILD,
|
|
shell_tool_name="run_shell_command",
|
|
shell_tool_prompt=_SHELL_PROMPT,
|
|
),
|
|
"kimi-native": _C(
|
|
_IM.NATIVE_TUI,
|
|
_EL.HOOK,
|
|
_RS.WARM_REATTACH,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.SESSION_SCOPED_CONFIG,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
fork_history=_FH.NONE,
|
|
shell_tool_name="Bash",
|
|
shell_tool_prompt=_BASH_PROMPT,
|
|
),
|
|
"opencode-native": _C(
|
|
_IM.NATIVE_SERVER,
|
|
_EL.SSE_PERMISSION,
|
|
_RS.WARM_REATTACH,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=True,
|
|
interrupt=True,
|
|
streaming=True,
|
|
fork_history=_FH.PREAMBLE,
|
|
# NATIVE_SERVER, not driven by the bench's native-tui tool probe, so
|
|
# shell_tool_* stay None.
|
|
),
|
|
"hermes-native": _C(
|
|
_IM.NATIVE_TUI,
|
|
_EL.APPROVAL_MIRROR,
|
|
_RS.WARM_REATTACH,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
fork_history=_FH.REBUILD,
|
|
shell_tool_name="terminal",
|
|
shell_tool_prompt=_SHELL_PROMPT,
|
|
),
|
|
# SDK / subprocess harnesses (run the vendor model directly). The first four
|
|
# are bench-verified interrupt=streaming=True.
|
|
"claude-sdk": _C(
|
|
_IM.SDK_IN_PROCESS,
|
|
_EL.NONE,
|
|
_RS.COLD_ONLY,
|
|
_EF.ANTHROPIC,
|
|
_MF.CLAUDE,
|
|
_AU.OMNIGENT_CREDENTIAL,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
"codex": _C(
|
|
_IM.CLI_SUBPROCESS,
|
|
_EL.JSONRPC,
|
|
_RS.WARM_REATTACH,
|
|
_EF.OPENAI,
|
|
_MF.GPT,
|
|
_AU.OMNIGENT_CREDENTIAL,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
"pi": _C(
|
|
_IM.CLI_SUBPROCESS,
|
|
_EL.NONE,
|
|
_RS.COLD_ONLY,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OMNIGENT_CREDENTIAL,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
"openai-agents": _C(
|
|
_IM.SDK_IN_PROCESS,
|
|
_EL.NONE,
|
|
_RS.COLD_ONLY,
|
|
_EF.OPENAI,
|
|
_MF.MULTI,
|
|
_AU.OMNIGENT_CREDENTIAL,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
"cursor": _C(
|
|
_IM.SDK_IN_PROCESS,
|
|
_EL.NONE,
|
|
_RS.WARM_REATTACH,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
"antigravity": _C(
|
|
_IM.SDK_IN_PROCESS,
|
|
_EL.NONE,
|
|
_RS.COLD_ONLY,
|
|
_EF.GEMINI,
|
|
_MF.GEMINI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
# Generic ACP harness — drives any user-configured ACP agent command. Same
|
|
# profile as goose/qwen (own-auth, cold resume, SSE permission), but its
|
|
# interrupt IS implemented (ACP ``session/cancel``), not just declared.
|
|
"acp": _C(
|
|
_IM.ACP_SUBPROCESS,
|
|
_EL.SSE_PERMISSION,
|
|
_RS.COLD_ONLY,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
"goose": _C(
|
|
_IM.ACP_SUBPROCESS,
|
|
_EL.SSE_PERMISSION,
|
|
_RS.COLD_ONLY,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
"qwen": _C(
|
|
_IM.ACP_SUBPROCESS,
|
|
_EL.SSE_PERMISSION,
|
|
_RS.COLD_ONLY,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
"kimi": _C(
|
|
_IM.CLI_SUBPROCESS,
|
|
_EL.NONE,
|
|
_RS.WARM_REATTACH,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.SESSION_SCOPED_CONFIG,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
"hermes": _C(
|
|
_IM.CLI_SUBPROCESS,
|
|
_EL.HOOK,
|
|
_RS.COLD_ONLY,
|
|
_EF.NONE,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
"copilot": _C(
|
|
_IM.SDK_IN_PROCESS,
|
|
_EL.NONE,
|
|
_RS.COLD_ONLY,
|
|
_EF.COPILOT,
|
|
_MF.MULTI,
|
|
_AU.OWN_AUTH,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
# open-responses is resolved via an alternate path, but its executor
|
|
# (omnigent/inner/open_responses_sdk.py) is concrete: interrupt_session()
|
|
# closes the active stream and returns True, supports_streaming() is True,
|
|
# and it drives an OpenAI Responses model (gpt-5.3-codex) forwarding
|
|
# reasoning_effort via cfg.extra — so effort is OPENAI.
|
|
"open-responses": _C(
|
|
_IM.SDK_IN_PROCESS,
|
|
_EL.NONE,
|
|
_RS.COLD_ONLY,
|
|
_EF.OPENAI,
|
|
_MF.MULTI,
|
|
_AU.OMNIGENT_CREDENTIAL,
|
|
subagents=False,
|
|
interrupt=True,
|
|
streaming=True,
|
|
),
|
|
}
|
|
|
|
# Builtin ACP CLI harnesses (omnigent/acp_cli_harnesses.py) run through the
|
|
# same generic wrap as the "acp" harness, so they share its declared profile.
|
|
for _acp_cli_name in ACP_CLI_HARNESSES:
|
|
_BUILTIN_CAPABILITIES[_acp_cli_name] = _BUILTIN_CAPABILITIES["acp"]
|
|
|
|
|
|
_BUILTIN_CONTRIBUTION = HarnessContribution(
|
|
name="omnigent",
|
|
valid_harnesses=frozenset(
|
|
{
|
|
"acp",
|
|
"antigravity",
|
|
"antigravity-native",
|
|
"claude-native",
|
|
"claude-sdk",
|
|
"codex",
|
|
"codex-native",
|
|
"copilot",
|
|
"cursor",
|
|
"cursor-native",
|
|
"goose",
|
|
"goose-native",
|
|
"hermes",
|
|
"hermes-native",
|
|
"kimi",
|
|
"kimi-native",
|
|
"kiro-native",
|
|
"open-responses",
|
|
"openai-agents",
|
|
"opencode-native",
|
|
"pi",
|
|
"pi-native",
|
|
"qwen",
|
|
"qwen-native",
|
|
}
|
|
# Builtin ACP CLI harnesses derive from the declarative catalog; a new
|
|
# vendor CLI is one row there, not another entry in each set below.
|
|
| set(ACP_CLI_HARNESSES)
|
|
),
|
|
harness_modules={
|
|
# Every catalog row runs the shared generic ACP wrap.
|
|
**dict.fromkeys(ACP_CLI_HARNESSES, "omnigent.inner.acp_harness"),
|
|
"acp": "omnigent.inner.acp_harness",
|
|
"antigravity": "omnigent.inner.antigravity_harness",
|
|
"antigravity-native": "omnigent.inner.antigravity_native_harness",
|
|
"claude-native": "omnigent.inner.claude_native_harness",
|
|
"claude-sdk": "omnigent.inner.claude_sdk_harness",
|
|
"codex": "omnigent.inner.codex_harness",
|
|
"codex-native": "omnigent.inner.codex_native_harness",
|
|
"copilot": "omnigent.inner.copilot_harness",
|
|
"cursor": "omnigent.inner.cursor_harness",
|
|
"cursor-native": "omnigent.inner.cursor_native_harness",
|
|
"goose": "omnigent.inner.goose_harness",
|
|
"goose-native": "omnigent.inner.goose_native_harness",
|
|
"hermes": "omnigent.inner.hermes_harness",
|
|
"hermes-native": "omnigent.inner.hermes_native_harness",
|
|
"kimi": "omnigent.inner.kimi_harness",
|
|
"kimi-native": "omnigent.inner.kimi_native_harness",
|
|
"kiro-native": "omnigent.inner.kiro_native_harness",
|
|
"openai-agents": "omnigent.inner.openai_agents_sdk_harness",
|
|
"opencode-native": "omnigent.inner.opencode_native_harness",
|
|
"pi": "omnigent.inner.pi_harness",
|
|
"pi-native": "omnigent.inner.pi_native_harness",
|
|
"qwen": "omnigent.inner.qwen_harness",
|
|
"qwen-native": "omnigent.inner.qwen_native_harness",
|
|
},
|
|
aliases={
|
|
**{alias: name for name, row in ACP_CLI_HARNESSES.items() for alias in row.aliases},
|
|
"agy": "antigravity",
|
|
"claude": "claude-sdk",
|
|
"github-copilot": "copilot",
|
|
"google-antigravity": "antigravity",
|
|
"kimi-code": "kimi",
|
|
"native-antigravity": "antigravity-native",
|
|
"native-goose": "goose-native",
|
|
"native-hermes": "hermes-native",
|
|
"native-kimi": "kimi-native",
|
|
"native-kiro": "kiro-native",
|
|
"native-opencode": "opencode-native",
|
|
"native-pi": "pi-native",
|
|
"native-qwen": "qwen-native",
|
|
"opencode": "opencode-native",
|
|
"openai-agents-sdk": "openai-agents",
|
|
"qwen-code": "qwen",
|
|
},
|
|
native_harnesses=frozenset(
|
|
{
|
|
"antigravity-native",
|
|
"claude-native",
|
|
"codex-native",
|
|
"cursor-native",
|
|
"goose-native",
|
|
"hermes-native",
|
|
"kimi-native",
|
|
"kiro-native",
|
|
"native-antigravity",
|
|
"native-claude",
|
|
"native-codex",
|
|
"native-cursor",
|
|
"native-goose",
|
|
"native-hermes",
|
|
"native-kimi",
|
|
"native-kiro",
|
|
"native-opencode",
|
|
"native-pi",
|
|
"native-qwen",
|
|
"opencode-native",
|
|
"pi-native",
|
|
"qwen-native",
|
|
}
|
|
),
|
|
native_agents=(
|
|
CLAUDE_NATIVE_CODING_AGENT,
|
|
CODEX_NATIVE_CODING_AGENT,
|
|
PI_NATIVE_CODING_AGENT,
|
|
OPENCODE_NATIVE_CODING_AGENT,
|
|
CURSOR_NATIVE_CODING_AGENT,
|
|
KIRO_NATIVE_CODING_AGENT,
|
|
GOOSE_NATIVE_CODING_AGENT,
|
|
ANTIGRAVITY_NATIVE_CODING_AGENT,
|
|
QWEN_NATIVE_CODING_AGENT,
|
|
KIMI_NATIVE_CODING_AGENT,
|
|
HERMES_NATIVE_CODING_AGENT,
|
|
),
|
|
native_providers=_BUILTIN_NATIVE_PROVIDERS,
|
|
# Catalog rows gate readiness on their vendor binary; the install spec also
|
|
# feeds setup steps and (for npm rows) the one-click install path.
|
|
install_specs={name: row.install for name, row in ACP_CLI_HARNESSES.items()},
|
|
harness_install_keys={
|
|
spelling: name
|
|
for name, row in ACP_CLI_HARNESSES.items()
|
|
for spelling in (name, *row.aliases)
|
|
},
|
|
model_env_keys={
|
|
"acp": "HARNESS_ACP_MODEL",
|
|
"antigravity": "HARNESS_ANTIGRAVITY_MODEL",
|
|
"claude-sdk": "HARNESS_CLAUDE_SDK_MODEL",
|
|
"codex": "HARNESS_CODEX_MODEL",
|
|
"copilot": "HARNESS_COPILOT_MODEL",
|
|
"cursor": "HARNESS_CURSOR_MODEL",
|
|
"goose": "HARNESS_GOOSE_MODEL",
|
|
"hermes": "HARNESS_HERMES_MODEL",
|
|
"kimi": "HARNESS_KIMI_MODEL",
|
|
"openai-agents": "HARNESS_OPENAI_AGENTS_MODEL",
|
|
"pi": "HARNESS_PI_MODEL",
|
|
"qwen": "HARNESS_QWEN_MODEL",
|
|
},
|
|
background_title_generators={
|
|
"claude-sdk": BackgroundTitleGeneratorSpec(
|
|
"omnigent.runner.background_titles.sdk:generate_background_title"
|
|
),
|
|
"claude-native": BackgroundTitleGeneratorSpec(
|
|
"omnigent.runner.background_titles.claude_native:generate_background_title",
|
|
resolver_harness="claude-sdk",
|
|
),
|
|
"codex": BackgroundTitleGeneratorSpec(
|
|
"omnigent.runner.background_titles.sdk:generate_background_title"
|
|
),
|
|
"codex-native": BackgroundTitleGeneratorSpec(
|
|
"omnigent.runner.background_titles.codex_native:generate_background_title"
|
|
),
|
|
},
|
|
harness_labels={
|
|
"antigravity": "Antigravity",
|
|
"claude-sdk": "Claude SDK",
|
|
"codex": "Codex",
|
|
"copilot": "Copilot",
|
|
"cursor": "Cursor",
|
|
"hermes": "Hermes",
|
|
# openai-agents is intentionally omitted from the picker catalog: it
|
|
# stays a valid harness for YAML specs (and the credential-free
|
|
# integration mock LLM), but is no longer offered as a UI pick.
|
|
"pi": "Pi",
|
|
**{name: row.label for name, row in ACP_CLI_HARNESSES.items()},
|
|
},
|
|
capabilities=_BUILTIN_CAPABILITIES,
|
|
)
|
|
|
|
_state: HarnessPluginState | None = None
|
|
|
|
|
|
def _entry_points() -> tuple[importlib.metadata.EntryPoint, ...]:
|
|
discovered = importlib.metadata.entry_points()
|
|
if hasattr(discovered, "select"):
|
|
return tuple(discovered.select(group=COMMUNITY_ENTRY_POINT_GROUP))
|
|
legacy = cast(
|
|
Mapping[str, Iterable[importlib.metadata.EntryPoint]],
|
|
discovered,
|
|
)
|
|
return tuple(legacy.get(COMMUNITY_ENTRY_POINT_GROUP, ()))
|
|
|
|
|
|
def _module_part(import_path: str) -> str:
|
|
return import_path.split(":", 1)[0]
|
|
|
|
|
|
def _community_paths(contribution: HarnessContribution) -> list[str]:
|
|
paths: list[str] = []
|
|
paths.extend(contribution.harness_modules.values())
|
|
paths.extend(contribution.spawn_env_builders.values())
|
|
paths.extend(spec.generator for spec in contribution.background_title_generators.values())
|
|
return paths
|
|
|
|
|
|
def _harness_spellings(contribution: HarnessContribution) -> set[str]:
|
|
"""Return every harness/alias key claimed by a contribution."""
|
|
return (
|
|
set(contribution.valid_harnesses)
|
|
| set(contribution.harness_modules)
|
|
| set(contribution.aliases)
|
|
| set(contribution.native_harnesses)
|
|
| set(contribution.harness_install_keys)
|
|
| set(contribution.model_env_keys)
|
|
| set(contribution.background_title_generators)
|
|
| set(contribution.missing_install_package)
|
|
| set(contribution.harness_labels)
|
|
| set(contribution.capabilities)
|
|
)
|
|
|
|
|
|
def _native_agent_identity_values(contribution: HarnessContribution) -> set[str]:
|
|
"""Return native-agent identifiers that must stay globally unique."""
|
|
values: set[str] = set()
|
|
for agent in contribution.native_agents:
|
|
values.update(
|
|
{
|
|
agent.key,
|
|
agent.agent_name,
|
|
agent.wrapper_label,
|
|
agent.terminal_name,
|
|
}
|
|
)
|
|
if agent.subagent_wrapper_label:
|
|
values.add(agent.subagent_wrapper_label)
|
|
return values
|
|
|
|
|
|
def _validate_community_contribution(
|
|
contribution: HarnessContribution,
|
|
*,
|
|
entry_point_name: str,
|
|
existing: tuple[HarnessContribution, ...],
|
|
) -> str | None:
|
|
if not contribution.name:
|
|
return "plugin contribution must set name"
|
|
|
|
for path in _community_paths(contribution):
|
|
if not _module_part(path).startswith(COMMUNITY_MODULE_PREFIX):
|
|
return (
|
|
f"community harness plugin {entry_point_name!r} uses import path "
|
|
f"{path!r}; expected {COMMUNITY_MODULE_PREFIX}*"
|
|
)
|
|
|
|
if contribution.native_harnesses or contribution.native_agents:
|
|
return (
|
|
f"community harness plugin {entry_point_name!r} registers native terminal "
|
|
"metadata, but community native terminal harnesses are not supported yet"
|
|
)
|
|
|
|
existing_harness_spellings: set[str] = set()
|
|
existing_install_keys: set[str] = set()
|
|
existing_native_agent_values: set[str] = set()
|
|
for accepted in existing:
|
|
existing_harness_spellings.update(_harness_spellings(accepted))
|
|
existing_install_keys.update(accepted.install_specs)
|
|
existing_native_agent_values.update(_native_agent_identity_values(accepted))
|
|
|
|
harness_collisions = existing_harness_spellings.intersection(_harness_spellings(contribution))
|
|
if harness_collisions:
|
|
return (
|
|
f"community harness plugin {entry_point_name!r} attempts to override "
|
|
f"existing harness keys: {sorted(harness_collisions)}"
|
|
)
|
|
|
|
install_key_collisions = existing_install_keys.intersection(contribution.install_specs)
|
|
if install_key_collisions:
|
|
return (
|
|
f"community harness plugin {entry_point_name!r} attempts to override "
|
|
f"existing install keys: {sorted(install_key_collisions)}"
|
|
)
|
|
|
|
native_agent_collisions = existing_native_agent_values.intersection(
|
|
_native_agent_identity_values(contribution)
|
|
)
|
|
if native_agent_collisions:
|
|
return (
|
|
f"community harness plugin {entry_point_name!r} attempts to override "
|
|
f"existing native-agent keys: {sorted(native_agent_collisions)}"
|
|
)
|
|
|
|
allowed_targets = set(contribution.valid_harnesses)
|
|
for harness, spec in contribution.background_title_generators.items():
|
|
if harness not in allowed_targets:
|
|
return (
|
|
f"community harness plugin {entry_point_name!r} registers background "
|
|
f"titles for undeclared harness {harness!r}"
|
|
)
|
|
if spec.resolver_harness is not None and spec.resolver_harness not in allowed_targets:
|
|
return (
|
|
f"community harness plugin {entry_point_name!r} background title resolver "
|
|
f"{spec.resolver_harness!r} is not contributed by the plugin"
|
|
)
|
|
for alias, target in contribution.aliases.items():
|
|
if target not in allowed_targets:
|
|
return (
|
|
f"community harness plugin {entry_point_name!r} alias {alias!r} "
|
|
f"targets {target!r}, which is not contributed by the plugin"
|
|
)
|
|
return None
|
|
|
|
|
|
def plugin_state() -> HarnessPluginState:
|
|
"""Return the merged built-in + community harness registry."""
|
|
global _state
|
|
if _state is not None:
|
|
return _state
|
|
|
|
contributions: list[HarnessContribution] = [_BUILTIN_CONTRIBUTION]
|
|
load_errors: dict[str, str] = {}
|
|
for entry_point in _entry_points():
|
|
try:
|
|
loaded = entry_point.load()
|
|
contribution = loaded() if callable(loaded) else loaded
|
|
if not isinstance(contribution, HarnessContribution):
|
|
raise TypeError(
|
|
f"entry point returned {type(contribution).__name__}, "
|
|
"expected HarnessContribution"
|
|
)
|
|
error = _validate_community_contribution(
|
|
contribution,
|
|
entry_point_name=entry_point.name,
|
|
existing=tuple(contributions),
|
|
)
|
|
if error is not None:
|
|
raise ValueError(error)
|
|
contributions.append(contribution)
|
|
except Exception as exc: # noqa: BLE001 - broken plugins must not break core startup.
|
|
load_errors[entry_point.name] = str(exc)
|
|
_logger.warning(
|
|
"could not load harness plugin entry point %s (%s)",
|
|
entry_point.name,
|
|
exc,
|
|
exc_info=True,
|
|
)
|
|
|
|
_state = HarnessPluginState(tuple(contributions), load_errors)
|
|
return _state
|
|
|
|
|
|
def reset_plugin_state_for_tests() -> None:
|
|
"""Clear the cached plugin state."""
|
|
global _state
|
|
_state = None
|
|
|
|
|
|
_Value = TypeVar("_Value")
|
|
|
|
|
|
def _merge_dict(
|
|
getter: Callable[[HarnessContribution], Mapping[str, _Value]],
|
|
) -> dict[str, _Value]:
|
|
merged: dict[str, _Value] = {}
|
|
for contribution in plugin_state().contributions:
|
|
merged.update(getter(contribution))
|
|
return merged
|
|
|
|
|
|
def _merge_set(
|
|
getter: Callable[[HarnessContribution], Iterable[str]],
|
|
) -> frozenset[str]:
|
|
merged: set[str] = set()
|
|
for contribution in plugin_state().contributions:
|
|
merged.update(getter(contribution))
|
|
return frozenset(merged)
|
|
|
|
|
|
def valid_harnesses() -> frozenset[str]:
|
|
"""Return canonical harness ids accepted by installed contributions."""
|
|
return _merge_set(lambda contribution: contribution.valid_harnesses)
|
|
|
|
|
|
def harness_aliases() -> dict[str, str]:
|
|
"""Return alias-to-canonical harness ids."""
|
|
return _merge_dict(lambda contribution: contribution.aliases)
|
|
|
|
|
|
def accepted_harnesses() -> frozenset[str]:
|
|
"""Return canonical harness ids plus accepted aliases."""
|
|
return valid_harnesses() | frozenset(harness_aliases())
|
|
|
|
|
|
def native_harnesses() -> frozenset[str]:
|
|
"""Return native CLI harness ids and native aliases."""
|
|
return _merge_set(lambda contribution: contribution.native_harnesses)
|
|
|
|
|
|
def native_agents() -> tuple[NativeCodingAgent, ...]:
|
|
"""Return native coding-agent metadata rows."""
|
|
agents: list[NativeCodingAgent] = []
|
|
for contribution in plugin_state().contributions:
|
|
agents.extend(contribution.native_agents)
|
|
return tuple(agents)
|
|
|
|
|
|
def native_providers() -> tuple[NativeHarnessProvider, ...]:
|
|
"""Return native-harness behavior provider rows, merged across contributions."""
|
|
providers: list[NativeHarnessProvider] = []
|
|
for contribution in plugin_state().contributions:
|
|
providers.extend(contribution.native_providers)
|
|
return tuple(providers)
|
|
|
|
|
|
def native_provider_for_key(key: str) -> NativeHarnessProvider | None:
|
|
"""Return the provider row whose ``key`` matches, or ``None``."""
|
|
for provider in native_providers():
|
|
if provider.key == key:
|
|
return provider
|
|
return None
|
|
|
|
|
|
def harness_modules() -> dict[str, str]:
|
|
"""Return runtime harness module mapping, aliases included."""
|
|
modules = _merge_dict(lambda contribution: contribution.harness_modules)
|
|
for alias, canonical in harness_aliases().items():
|
|
module = modules.get(canonical)
|
|
if module is not None:
|
|
modules.setdefault(alias, module)
|
|
return modules
|
|
|
|
|
|
def model_env_keys() -> dict[str, str]:
|
|
"""Return harness-to-model-env-var mapping."""
|
|
return _merge_dict(lambda contribution: contribution.model_env_keys)
|
|
|
|
|
|
def spawn_env_builders() -> dict[str, str]:
|
|
"""Return harness-to-spawn-env-builder import paths."""
|
|
return _merge_dict(lambda contribution: contribution.spawn_env_builders)
|
|
|
|
|
|
def background_title_generators() -> dict[str, BackgroundTitleGeneratorSpec]:
|
|
"""Return harness-to-background-title-generator registrations."""
|
|
return _merge_dict(lambda contribution: contribution.background_title_generators)
|
|
|
|
|
|
def install_specs() -> dict[str, HarnessInstallSpec]:
|
|
"""Return plugin-provided install specs."""
|
|
return _merge_dict(lambda contribution: contribution.install_specs)
|
|
|
|
|
|
def harness_install_keys() -> dict[str, str]:
|
|
"""Return harness/alias to install-spec key mappings."""
|
|
return _merge_dict(lambda contribution: contribution.harness_install_keys)
|
|
|
|
|
|
def missing_install_packages() -> dict[str, str]:
|
|
"""Return optional harness spellings to package names."""
|
|
return _merge_dict(lambda contribution: contribution.missing_install_package)
|
|
|
|
|
|
def harness_labels() -> dict[str, str]:
|
|
"""Return labels for non-native harness picker/catalog rows."""
|
|
return _merge_dict(lambda contribution: contribution.harness_labels)
|
|
|
|
|
|
def harness_capabilities() -> dict[str, HarnessCapabilities]:
|
|
"""Return the declared capability record per harness id.
|
|
|
|
Merged across contributions; sparse (a harness need not declare
|
|
capabilities). This is the single source of truth for "what can this
|
|
harness do?".
|
|
"""
|
|
return _merge_dict(lambda contribution: contribution.capabilities)
|
|
|
|
|
|
def harness_catalog() -> list[dict[str, object]]:
|
|
"""Return stable JSON-serializable harness catalog rows.
|
|
|
|
Each row carries ``id`` and ``label``; rows for harnesses with declared
|
|
capabilities also carry a ``capabilities`` object (see
|
|
:meth:`HarnessCapabilities.as_dict`). ``setup_steps`` lists the ordered
|
|
requirements to get the harness ready on a host (install + auth), so the
|
|
web UI can render a "set up this agent" checklist that mirrors
|
|
``omnigent setup``; the host reports each step's status in its readiness map.
|
|
"""
|
|
labels = harness_labels()
|
|
capabilities = harness_capabilities()
|
|
# Lazy import for the same reason as the acp rows below: keep this registry
|
|
# importable without pulling in the onboarding/config stack at module load.
|
|
try:
|
|
from omnigent.onboarding.harness_install import ui_setup_steps
|
|
except Exception: # noqa: BLE001 — a broken onboarding import must not break the catalog
|
|
_logger.debug("setup-step metadata unavailable", exc_info=True)
|
|
ui_setup_steps = None # type: ignore[assignment]
|
|
rows: list[dict[str, object]] = []
|
|
for harness in sorted(labels, key=lambda key: labels[key].lower()):
|
|
if harness not in valid_harnesses():
|
|
continue
|
|
row: dict[str, object] = {"id": harness, "label": labels[harness]}
|
|
capability = capabilities.get(harness)
|
|
if capability is not None:
|
|
row["capabilities"] = capability.as_dict()
|
|
if ui_setup_steps is not None:
|
|
row["setup_steps"] = [step.as_dict() for step in ui_setup_steps(harness)]
|
|
rows.append(row)
|
|
|
|
# Dynamic rows: one per user-configured generic-ACP agent, id ``acp:<slug>``.
|
|
# The base ``acp`` harness deliberately has no ``harness_labels`` entry, so it
|
|
# is not a standalone picker row — only the configured agents surface. Read
|
|
# lazily so importing this registry never pulls in the onboarding/config
|
|
# stack, and never let a malformed ``acp:`` block break the whole catalog.
|
|
acp_capability = capabilities.get("acp")
|
|
try:
|
|
from omnigent.onboarding.acp_auth import acp_agents
|
|
|
|
for agent in acp_agents():
|
|
acp_row: dict[str, object] = {"id": f"acp:{agent.slug}", "label": agent.name}
|
|
if acp_capability is not None:
|
|
acp_row["capabilities"] = acp_capability.as_dict()
|
|
rows.append(acp_row)
|
|
except Exception: # noqa: BLE001 — a malformed acp: block must never break the catalog
|
|
_logger.debug("acp catalog rows skipped", exc_info=True)
|
|
return rows
|
|
|
|
|
|
def harness_setup_steps_by_spelling() -> dict[str, list[dict[str, str | None]]]:
|
|
"""Map every harness spelling to its ordered UI setup steps.
|
|
|
|
The web setup dialog looks steps up by the harness a *session* declares —
|
|
which is often a native wrapper (``codex-native``) or an installable id
|
|
that is not a picker row (``opencode``/``qwen``), neither of which appears
|
|
in :func:`harness_catalog`. Keying by spelling here lets the dialog resolve
|
|
steps for whatever id it holds. Values mirror ``harness_catalog``'s
|
|
``setup_steps`` (same :func:`ui_setup_steps` source), so the two can't
|
|
drift.
|
|
|
|
:returns: ``{spelling: [step.as_dict(), ...]}`` for every accepted spelling;
|
|
empty when the onboarding stack can't be imported (fail-open).
|
|
"""
|
|
try:
|
|
from omnigent.onboarding.harness_install import ui_installable_harnesses, ui_setup_steps
|
|
except Exception: # noqa: BLE001 — a broken onboarding import must not break the catalog
|
|
_logger.debug("setup-step metadata unavailable", exc_info=True)
|
|
return {}
|
|
# Cover the picker ids (catalog rows) plus every installable spelling
|
|
# (bare + native), so a session's declared harness always resolves.
|
|
spellings: set[str] = set(valid_harnesses())
|
|
spellings.update(ui_installable_harnesses())
|
|
return {
|
|
spelling: [step.as_dict() for step in ui_setup_steps(spelling)] for spelling in spellings
|
|
}
|
|
|
|
|
|
def load_object(import_path: str) -> object:
|
|
"""Load ``module:attribute`` or ``module.attribute``."""
|
|
if ":" in import_path:
|
|
module_name, attr = import_path.split(":", 1)
|
|
else:
|
|
module_name, attr = import_path.rsplit(".", 1)
|
|
module = importlib.import_module(module_name)
|
|
return getattr(module, attr)
|