Files
Pat Sukprasert 9a3c90594f refactor(harness): fork_history + shell-tool capability axes; derive gating (PR 1.8) (#3648)
Final Phase-1 PR of the modular native-harness registry refactor: move
registry-parallel enumerations onto HarnessCapabilities.

- Add a fork_history axis (ForkHistory enum: none/rebuild/preamble) to
  HarnessCapabilities, declared per harness in _BUILTIN_CAPABILITIES. Derive the
  server's two fork-history gating frozensets in _sessions/common.py from it
  instead of hand-listing. The derivation emits each canonical id plus its
  reversed native-<key> spelling, because native-claude/native-codex/native-cursor
  are valid ids canonicalize_harness passes through unchanged and the read sites
  match on the canonicalized id (guarded by the existing reversed-spelling fork
  test) — so the derived sets are a superset of the prior literals.
- Add optional shell_tool_name / shell_tool_prompt fields carrying the harness
  bench's shell-tool provocation; delete the bench's hardcoded
  _NATIVE_TOOL_PROVOCATION table and read the fields off capabilities in
  native_vendor() (byte-identical (tool_name, prompt) per harness).
- Delete the dead _HARNESS_MODULES literal in runtime/harnesses/__init__.py
  (~120 lines, overwritten unconditionally by harness_modules() next line).
- Extend the drift-guard tests in test_harness_capabilities.py.

Scope kept tight to the doc's mandate: sets that would need new NativeCodingAgent
identity fields (_ANTIGRAVITY_FAMILY_HARNESSES, _PROVIDER_RESOLUTION_HARNESS,
*_NATIVE_TERMINAL_ROLE) are left as-is; noted as follow-ups.

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-31 06:00:17 +00:00

161 lines
6.7 KiB
Python

"""Import-safe declarative capability model for harness plugins.
A harness's feature support was previously implicit — scattered across
``if harness == "x"`` branches and the presence/absence of companion modules
(``codex_native_elicitation.py``, ``*_native_hook.py``, ``*_native_permissions.py``).
This module gives it one declared shape so the registry can answer "what can
this harness do?" directly.
Like :mod:`omnigent.harness_install_spec`, this type lives outside the
onboarding/provider stack so an optional harness plugin can declare its
capabilities during entry-point discovery without triggering import cycles.
Each :class:`~omnigent.harness_plugins.HarnessContribution` carries a
per-harness ``capabilities`` map of these records.
The axes align with the harness-integration-guide feature matrix.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class IntegrationMode(str, Enum):
"""How the harness runs the vendor agent."""
SDK_IN_PROCESS = "sdk-in-process" # vendor SDK inside the harness subprocess
CLI_SUBPROCESS = "cli-subprocess" # drives a vendor CLI per turn
ACP_SUBPROCESS = "acp-subprocess" # vendor CLI in Agent Client Protocol mode
NATIVE_TUI = "native-tui" # wraps a resident vendor TUI (tmux / file-inject)
NATIVE_SERVER = "native-server" # runner-owned vendor server + HTTP/SSE bridge
class Elicitation(str, Enum):
"""How a policy ASK / tool-approval is surfaced to the Omnigent web UI."""
NONE = "none"
HOOK = "hook" # vendor PreToolUse hook posts to Omnigent
JSONRPC = "jsonrpc" # app-server JSON-RPC elicitation (codex)
APPROVAL_MIRROR = "approval-mirror" # poll the TUI approval pane, mirror to web
SSE_PERMISSION = "sse-permission" # permission events over SSE / ACP elicit
class Resume(str, Enum):
"""Whether a prior conversation is reattached or rebuilt."""
NONE = "none" # prior conversations cannot be resumed
WARM_REATTACH = "warm-reattach" # reattach to a live vendor session / terminal
COLD_ONLY = "cold-only" # rebuild from Omnigent transcript / history replay
class EffortFamily(str, Enum):
"""Which reasoning-effort value set applies (see reasoning_effort.py)."""
NONE = "none"
ANTHROPIC = "anthropic"
OPENAI = "openai"
GEMINI = "gemini"
COPILOT = "copilot"
class ModelFamily(str, Enum):
"""Which model vendors the harness accepts (see model_override.py)."""
CLAUDE = "claude"
GPT = "gpt"
GEMINI = "gemini"
MULTI = "multi" # accepts any validated id (no family rejection)
class AuthModel(str, Enum):
"""Where the harness's credentials come from."""
OMNIGENT_CREDENTIAL = "omnigent-credential" # Omnigent gateway / provider config
OWN_AUTH = "own-auth" # vendor login / API key, not Omnigent-managed
SESSION_SCOPED_CONFIG = "session-scoped-config" # per-session synthesized vendor config
class ForkHistory(str, Enum):
"""How a fork (or in-place agent switch) carries prior history into the harness."""
NONE = "none" # fork launches fresh; no prior turns are carried
REBUILD = "rebuild" # rebuild the vendor's resumable session file from copied items
PREAMBLE = "preamble" # replay prior turns as a text preamble (server-backed vendors)
@dataclass(frozen=True)
class HarnessCapabilities:
"""The declared feature set one harness supports.
:param integration_mode: How the harness runs the vendor agent.
:param elicitation: How a policy ASK is surfaced to the web UI.
:param resume: Whether a prior conversation is reattached or rebuilt.
:param effort: Which reasoning-effort value set applies.
:param model_family: Which model vendors the harness accepts.
:param auth: Where the harness's credentials come from.
:param subagents: Whether the harness can spawn Omnigent sub-agents.
:param interrupt: Whether a running turn can be cancelled mid-stream. This
is a *declared* claim; the harness bench's interrupt probe verifies it
live and flags drift when a harness does not honor it.
:param streaming: Whether the harness forwards token-level deltas (vs a
single complete blob). Declared claim; verified by the bench's
streaming probe.
:param steering: Whether input can be added to an active turn.
:param live_queue: Whether follow-up input can be queued during an active
turn.
:param images: Whether the harness accepts image input.
:param compaction: Whether the harness can compact conversation history.
Optional capability fields use ``None`` when the harness makes no claim;
the bench reports those declarations as ``UNKNOWN`` rather than assuming
the capability is unsupported.
:param fork_history: How a fork / in-place agent switch carries prior history
into the harness — ``none`` (fresh), ``rebuild`` (rebuild the vendor's
resumable session file from copied items), or ``preamble`` (replay prior
turns as a text preamble). Drives the server's fork-history gating.
:param shell_tool_name: The harness's shell/exec tool name the harness bench
provokes to verify tool-calling (e.g. ``"Bash"``, ``"shell"``). ``None``
skips the bench's tool/policy probe for this harness.
:param shell_tool_prompt: The prompt the bench sends to provoke that tool.
Must contain the ``omnigent-bench-ok`` placeholder the probe token-swaps.
``None`` skips the probe.
"""
integration_mode: IntegrationMode
elicitation: Elicitation
resume: Resume
effort: EffortFamily
model_family: ModelFamily
auth: AuthModel
subagents: bool
interrupt: bool
streaming: bool
steering: bool | None = None
live_queue: bool | None = None
images: bool | None = None
compaction: bool | None = None
fork_history: ForkHistory = ForkHistory.NONE
shell_tool_name: str | None = None
shell_tool_prompt: str | None = None
def as_dict(self) -> dict[str, str | bool | None]:
"""Return a JSON-serializable view for the ``/v1/harnesses`` catalog."""
return {
"integration_mode": self.integration_mode.value,
"elicitation": self.elicitation.value,
"resume": self.resume.value,
"effort": self.effort.value,
"model_family": self.model_family.value,
"auth": self.auth.value,
"subagents": self.subagents,
"interrupt": self.interrupt,
"streaming": self.streaming,
"steering": self.steering,
"live_queue": self.live_queue,
"images": self.images,
"compaction": self.compaction,
"fork_history": self.fork_history.value,
"shell_tool_name": self.shell_tool_name,
"shell_tool_prompt": self.shell_tool_prompt,
}