Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,36 @@ To configure specific tools without the picker, pass a comma-separated list:
ug configure --agents claude,codex
```

Available agent names are `codex`, `claude`, `gemini`, `opencode`, `copilot`, and `pi`. `cursor` is also accepted (MCP-only — it registers Databricks MCP servers but configures no models).
Available agent names are `codex`, `claude`, `gemini`, `opencode`, `copilot`, `pi`, and `hermes`. `cursor` is also accepted (MCP-only — it registers Databricks MCP servers but configures no models).

### Hermes Agent

Hermes exposes **Databricks Model Serving** in its native provider picker and
delegates Databricks-specific setup to ucode. Configure it directly without
launching a session with:

```bash
ucode configure hermes
```

For automation, pin the workspace, Databricks CLI profile, model, and isolated
Hermes home explicitly:

```bash
ucode configure hermes \
--workspace https://my-workspace.databricks.com \
--profile DEFAULT \
--model system.ai.my-model \
--hermes-home /path/to/hermes-home \
--output json
```

Generated providers use refreshable Databricks CLI credentials; ucode never
writes an OAuth token or PAT into Hermes configuration. Normal use remains
`hermes` and its `/model` picker—no ucode wrapper process is required.

Current support includes Responses/Codex, Anthropic Messages, native Gemini,
OpenAI-compatible OSS models, and managed MCP servers.

Naming agents explicitly is treated as a request for all of them: if any one isn't available on the workspace, the run fails without configuring the others. Add `--skip-unavailable` to configure the available subset instead and skip the rest with a warning:

Expand Down
156 changes: 156 additions & 0 deletions docs/hermes-agent-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Hermes Agent integration

## Status and scope

This document defines the cross-repository contract for configuring Hermes to
use Databricks-governed models. The user-facing provider name is **Databricks
Model Serving**. Runtime traffic uses Unity AI Gateway routes; the display name
does not change that architecture.

The integration supports route-specific providers for OpenAI Responses,
Anthropic Messages, native Gemini, and OpenAI-compatible OSS chat. It also
registers ucode-managed MCP servers through Hermes's native configuration
interface.

## Ownership boundary

ucode owns all Databricks-specific behavior:

- Databricks CLI profiles, OAuth/PAT authentication, and token refresh
- workspace capability and model discovery
- Gateway route and wire-protocol selection
- Databricks-specific request headers
- managed MCP discovery and registration
- validation, diagnostics, managed-key tracking, and cleanup
- rendering the non-secret patch applied to Hermes configuration

Hermes owns its configuration schema and mutation semantics. It exposes a
native provider entry and a thin, argv-based setup handoff to ucode. After
setup, Hermes resolves the generated providers through its existing generic
transports. Hermes must not add a Databricks transport, static model catalog,
runtime token-refresh branch, or Databricks-specific auxiliary/delegation path.

Normal sessions run with `hermes`; no `ucode hermes` wrapper is required.

## Command contract

The stable setup entry point is:

```text
ucode configure hermes [options]
```

The command configures Hermes and exits without launching it. It must:

- use argv semantics throughout and never invoke a shell;
- accept an explicit Hermes home/profile target for automation and isolation;
- prompt for missing workspace, profile, or model inputs in interactive use;
- support fully specified non-interactive use;
- be idempotent and modify only ucode-managed keys;
- support surgical unconfiguration of those keys;
- return nonzero without changing the active provider/model on failure or
cancellation; and
- never expose access tokens, refresh tokens, PATs, or token-helper output.

Machine-readable completion output is additive/versionable and contains only
non-secret state. The initial shape is:

```json
{
"status": "configured",
"agent": "hermes",
"hermes_home": "/resolved/path",
"provider_ids": ["ucode-databricks-codex"],
"default_provider": "ucode-databricks-codex",
"default_model": "system.ai.<model>",
"mcp_servers_configured": [],
"warnings": []
}
```

Hermes relies on the child exit status, then reloads and validates its own
configuration. It does not duplicate Databricks field validation.

## Configuration transaction

ucode does not edit Hermes YAML directly and does not import Hermes Python
modules. It invokes Hermes's public `config get`, `config set`, and
`config unset` commands with an explicit `HERMES_HOME`.

Hermes does not currently expose an atomic multi-key transaction. ucode checks
that new managed paths are absent and that previously managed values still
match their recorded fingerprints before mutation. Provider definitions are
written before switching the active model. Cleanup removes only values whose
fingerprints still match; uncertain ownership preserves configuration rather
than deleting a possible user replacement. A process failure after mutation
starts can therefore leave a partial update, which is surfaced as an error for
explicit reconciliation.

## Provider representation

Each wire protocol has a separate generated custom provider so every model is
coupled to the correct Gateway route and Hermes transport:

- `ucode-databricks-codex` for OpenAI Responses;
- `ucode-databricks-anthropic` for Anthropic Messages;
- `ucode-databricks-gemini` for native Gemini; and
- `ucode-databricks-oss` for OpenAI-compatible OSS chat.

Hermes retains its internal `provider="custom"` identity while preserving the
selected generated provider separately. Each provider obtains credentials from
a refreshable `ucode auth-token` command; no Databricks token is stored in
Hermes configuration.

## Capability contract

| Capability | Release requirement | Owner | Acceptance evidence |
|---|---|---|---|
| Databricks CLI OAuth | Supported | ucode | Refresh succeeds after token cache invalidation/expiry |
| PAT compatibility | Preserve when explicitly selected | ucode | No PAT is written to Hermes config or output |
| Responses route | Supported | ucode config + Hermes generic transport | Streaming text and tool calls pass through `/ai-gateway/codex/v1` |
| Model-service discovery | Supported | ucode | `/model` contains the authorized coding models |
| Profile-scoped config | Supported | ucode + Hermes config commands | Default and named profiles remain isolated |
| Multiplex isolation | Supported | Hermes generic secret/config resolution | A scoped miss cannot borrow another profile's bearer/config |
| Reconfigure/unconfigure | Supported | ucode | Reconfigure is idempotent; cleanup removes only managed keys |
| Model Provider Service header routing | Deferred for Hermes v1 | ucode | No support claim until a production setup path exists |
| Unity Catalog permissions | Required | Gateway | Allowed models succeed and denied models fail clearly |
| Usage/inference tables | Required where enabled | Gateway, validated by ucode/E2E | Request is visible with expected identity and model |
| Rate limits and policies | Required pass-through | Gateway | 429 classification is preserved with no local bypass |
| Anthropic Messages route | Supported | ucode config + Hermes Messages transport | Claude streaming and tools pass |
| Gemini route | Supported | ucode config + Hermes Gemini transport | Gemini streaming and tools pass |
| OSS/OpenAI-compatible route | Supported | ucode config + Hermes chat transport | Supported OSS chat/tools pass |
| Managed MCP servers | Supported | ucode | Configured tools appear in Hermes without stored bearer tokens |
| Smart routing | Explicit decision gate | ucode | Tested support or explicit unsupported documentation |
| MLflow tracing hooks | Explicit decision gate | ucode | No support claim without Hermes-compatible trace evidence |
| Managed workspace manifest | Explicit decision gate | ucode | Decide whether Hermes belongs in the managed agent schema |

## Failure and security behavior

- Missing ucode is handled by Hermes with official installation guidance and no
partial configuration.
- Missing Databricks CLI/login, authorization failures, and empty discovery are
diagnosed by ucode.
- Hermes suppresses raw child output on failure; tests use sentinel secrets and
assert their absence from stdout, stderr, exceptions, fixtures, and config.
- No short-lived token or PAT is persisted in Hermes configuration.
- Provider discovery and import have no subprocess, OAuth, network, install, or
configuration side effects.

## Support boundaries

The two currently incompatible models, `gpt-oss-20b` and `gpt-oss-120b`, are
excluded from Hermes provider generation only. Their availability to unrelated
ucode consumers is unchanged. Smart routing, MLflow tracing hooks, and managed
workspace manifests remain separate decisions and are not implied by this
integration.

Databricks workspace authentication, discovery, Gateway routing, policies, and
managed MCP behavior are supported in ucode/Databricks. Generic Hermes session,
transport, and tool behavior remain supported in Hermes.

Primary references:

- [Databricks coding-agent integrations](https://docs.databricks.com/aws/en/ai-gateway/coding-agent-integration-model-services)
- [Databricks Model Provider Services](https://docs.databricks.com/aws/en/ai-gateway/coding-agent-integration-model-provider-services)
- [ucode](https://github.com/databricks/ucode)
- [Hermes Agent](https://github.com/NousResearch/hermes-agent)
35 changes: 32 additions & 3 deletions src/ucode/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
spinner,
)

from . import claude, codex, copilot, gemini, opencode, pi
from . import claude, codex, copilot, gemini, hermes, opencode, pi
from .args import explicit_model_arg_value as explicit_model_arg_value

_MODULES = {
Expand All @@ -50,6 +50,7 @@
"opencode": opencode,
"copilot": copilot,
"pi": pi,
"hermes": hermes,
}

TOOL_SPECS: dict[str, ToolSpec] = {name: module.SPEC for name, module in _MODULES.items()}
Expand All @@ -68,6 +69,7 @@
"opencode": "opencode",
"copilot": "copilot",
"pi": "pi",
"hermes": "hermes",
}

DEFAULT_TOOL = "codex"
Expand Down Expand Up @@ -100,7 +102,8 @@ def normalize_tool(tool: str) -> str:
normalized = TOOL_ALIASES.get(tool.strip().lower())
if not normalized:
raise RuntimeError(
f"Unsupported tool '{tool}'. Use one of: codex, claude, gemini, opencode, copilot, pi."
f"Unsupported tool '{tool}'. Use one of: "
"codex, claude, gemini, opencode, copilot, pi, hermes."
)
return normalized

Expand Down Expand Up @@ -184,6 +187,18 @@ def install_tool_binary(
binary = spec["binary"]
package = spec["package"]

if spec.get("install_method") == "external":
if shutil.which(binary):
return True
message = (
f"{spec['display']} is not installed (`{binary}` was not found on PATH). "
"Install Hermes using its official installer, then retry."
)
if strict:
raise RuntimeError(message)
print_warning(message)
return False

if shutil.which(binary):
# A too-new build is a correctness blocker (the tool runs but misbehaves
# against the gateway), so check it on every launch — not just when
Expand Down Expand Up @@ -236,6 +251,11 @@ def ensure_tool_binary_available(tool: str) -> None:
binary = spec["binary"]
if shutil.which(binary):
return
if spec.get("install_method") == "external":
raise RuntimeError(
f"{spec['display']} is not installed (`{binary}` was not found on PATH). "
"Install Hermes using its official installer, then retry."
)
raise RuntimeError(
f"{spec['display']} is not installed (`{binary}` was not found on PATH). "
f"Install it with `npm install -g {spec['package']}` or run "
Expand Down Expand Up @@ -432,13 +452,16 @@ def configure_tool(
# Every tool in this branch needs a model — including gemini under a provider,
# which still pins the service's target model in the URL.
if not model:
raise RuntimeError(f"A {tool} model must be selected before configuration.")
display = TOOL_SPECS[tool]["display"]
raise RuntimeError(f"A {display} model must be selected before configuration.")
if tool == "gemini":
result = gemini.write_tool_config(state, model, provider=provider)
elif tool == "copilot":
result = copilot.write_tool_config(state, model)
elif tool == "pi":
result = pi.write_tool_config(state, model)
elif tool == "hermes":
result = hermes.write_tool_config(state, model)
else:
result = opencode.write_tool_config(state, model)
# gemini/opencode/copilot/pi return (state, token); codex/claude return state
Expand All @@ -459,6 +482,11 @@ def check_gateway_endpoint(state: dict, tool: str) -> bool:
return bool(state.get("opencode_models"))
if tool == "codex":
return bool(state.get("codex_models"))
if tool == "hermes":
return any(
bool(state.get(key))
for key in ("codex_models", "claude_models", "gemini_models", "oss_models")
)
if tool == "gemini":
return bool(state.get("gemini_models"))
if tool == "copilot":
Expand All @@ -479,6 +507,7 @@ def check_gateway_endpoint(state: dict, tool: str) -> bool:
"gemini": ("gemini",),
"copilot": ("claude", "codex"),
"pi": ("claude", "codex", "gemini"),
"hermes": ("codex", "claude", "gemini", "oss"),
}


Expand Down
1 change: 1 addition & 0 deletions src/ucode/agents/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,7 @@ def _launch_relayed(state: dict, binary: str, tool_args: list[str]) -> None:
port,
token_header=gateway_proxy.AI_GATEWAY_TOKEN_HEADER,
force_refresh_near_expiry=False,
allow_env_bearer=bool(state.get("use_pat")),
)
# start_proxy falls back to an OS-assigned port when the cached one is taken
# (stale proxy from a killed session). Reconcile settings + state to whatever
Expand Down
Loading
Loading