Help us shape the future of Go! Take the 2026 Go Developer Survey

toolhivellm

package
v0.0.51 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package toolhivellm is the ToolHive-aware code in mecatl (issue #262 + #265): it detects, by reading ToolHive's OWN on-disk config file, whether a ToolHive LLM gateway proxy is set up for this user — and, if so, what LOOPBACK port it listens on — and (issue #265, tokensource.go) builds an in-process OIDC token source so both ToolHive protocol providers can talk DIRECTLY to the real gateway_url with no local proxy hop. Live listing remains owned by the wire adapters: internal/adapter/openaicompat for `toolhive` and provider/anthropic for `toolhive-anthropic`; this package supplies only detected routing intent.

What this reads

ToolHive's config file (default: platform user-config dir + toolhive/config.yaml — e.g. $XDG_CONFIG_HOME or ~/.config on Linux, ~/Library/Application Support on macOS, %AppData% on Windows, mirroring ToolHive's own github.com/adrg/xdg-based resolution — composition resolves this via stdlib os.UserConfigDir(), not this package) carries an `llm:` block:

llm:
  gateway_url: https://my-org.example.com/toolhive-gateway
  proxy:
    listen_port: 14000

gateway_url is the UPSTREAM the proxy forwards to (captured here for DIAGNOSTIC display only — R5.1/R5.2: it is NEVER used to construct a request URL). listen_port is the proxy's LOCAL loopback listen port (default 14000 when absent/zero).

Security invariants (v1-mandatory)

  • BaseURL() is ALWAYS "http://127.0.0.1:<port>/v1" — the host is HARDCODED, never derived from gateway_url or any other config value.
  • tls_skip_verify and any oidc/auth subtree are NEVER decoded — the wire struct below has no field for them, so even a config file that sets tls_skip_verify: true cannot influence anything (there is nowhere for the value to land).
  • The config file must be a REGULAR file, size-bounded, and OWNED BY THE CALLING UID before a single byte of it is trusted — a config file another user could plant or a mounted volume permission mistake never gets read.
  • Unknown YAML keys are silently ignored (no KnownFields strictness): a newer ToolHive config schema must not break detection here.

Layering

Stdlib + github.com/goccy/go-yaml ONLY. No domain, no port, no internal/app, no other adapter. The ONE invariant this file (detect.go) holds: NO ToolHive Go import, ever — the OIDC/token-source half lives in tokensource.go, the sole file in this package (and one of two in the tree, alongside internal/adapter/mcp/source/toolhive.go) allowed to import github.com/stacklok/toolhive. This detector never decodes the oidc/auth subtree (the wire struct below has no field for them); the OIDC-presence check reads toolhive's own config over in tokensource.go.

Index

Constants

View Source
const (
	// DefaultConfigRelPath is ToolHive's config file location, relative to the
	// platform user-config dir (composition joins it with os.UserConfigDir()).
	DefaultConfigRelPath = "toolhive/config.yaml"

	// PlaceholderToken is the public inbound-auth convention the ToolHive LLM
	// gateway proxy documents for a bearer credential when no real per-user
	// token is otherwise configured. It is not a secret; sending it (or
	// omitting it) never grants privilege the proxy itself doesn't already
	// extend to the caller.
	PlaceholderToken = "thv-proxy"
)
View Source
const ErrTokenRequiredHint = "no cached ToolHive LLM gateway credential — run `thv llm setup` to log in, or use `--toolhive-llm-mode proxy`"

ErrTokenRequiredHint is the actionable remediation surfaced when the non-interactive token source returns llm.ErrTokenRequired (no cached credential and the browser flow is disabled). It names BOTH remediations so a headless operator sees the exact next step, mirroring errToolhiveNoModels. It is a remediation HINT naming the next operator action, not a hardcoded credential — it carries no secret.

Variables

This section is empty.

Functions

func OIDCConfigured

func OIDCConfigured(configPath string) bool

OIDCConfigured reports whether the ToolHive LLM config at configPath has the minimum OIDC trio (gateway_url + issuer + client_id) required for direct mode — the same llm.Config.IsConfigured() the `thv` CLI gates on. It is the composition gate resolveToolhiveIntent calls to discriminate direct vs proxy under mode=auto. A config read failure fails CLOSED (returns false): a missing/malformed config falls back to proxy mode, never silently routes to a gateway with no credential.

func RunInteractiveLogin

func RunInteractiveLogin(ctx context.Context, configPath string, skipBrowser bool, diag port.Diagnostics) error

RunInteractiveLogin runs the interactive OIDC browser flow in-process (the SAME buildTokenSource pipeline with interactive=true) and obtains a fresh access token only to complete and cache the enrollment. It never prints or returns that token; machine consumers must use ToolHive's explicit token tooling. It is CLI-ONLY and does not start a session or connect to a server. skipBrowser prints the authorization URL instead of opening a browser (headless/SSH/CI). The tokenRefUpdater persists the rotated refresh-token reference so a subsequent non-interactive DirectTokenSource call finds the credential without re-login.

Types

type Config

type Config struct {
	// GatewayURL is the upstream the proxy forwards to. DIAGNOSTIC DISPLAY
	// ONLY — never used to construct a request URL (R5.1/R5.2).
	GatewayURL string
	// ListenPort is the proxy's local loopback listen port (already resolved
	// to defaultListenPort by DetectConfig when the config left it unset).
	ListenPort int
}

Config is the detected ToolHive LLM proxy configuration: the loopback port to probe, plus the upstream gateway_url captured for diagnostic display only.

func DetectConfig

func DetectConfig(path string) (Config, bool)

DetectConfig reads ToolHive's config file at path and reports whether an `llm:` block with a non-empty gateway_url was found. It is two-value, no error: EVERY failure mode (missing file, wrong type, oversized, wrong owner, malformed YAML, missing/empty llm.gateway_url, out-of-range port) is the SAME fail-soft "skip detection" outcome — the caller logs one DEBUG diagnostic on a miss, never surfaces a file-parsing error to the operator.

Sequence (each step fails closed, never open):

  1. os.Stat(path) — must exist.
  2. Mode().IsRegular() — never a directory/device/pipe/symlink-to-non-regular.
  3. Size() <= maxConfigBytes.
  4. Owning uid == the CALLING process's uid (statOwner; a failed type-assertion — e.g. non-unix — fails closed, never "trust it").
  5. Open + io.LimitReader(maxConfigBytes) read (defense-in-depth against a TOCTOU size change between Stat and Open).
  6. Typed YAML decode into wireConfig (unknown fields ignored).
  7. llm.gateway_url non-empty, else a miss.
  8. llm.proxy.listen_port: 0/absent -> defaultListenPort; out of [1,65535] -> a miss (an explicitly bogus port is a config error, not a silent floor).

func (Config) BaseURL

func (c Config) BaseURL() string

BaseURL returns the HARDCODED loopback base URL to probe/list/route requests through: "http://127.0.0.1:<port>/v1". The host is NEVER derived from GatewayURL or any other config value — this is the ONE security invariant this method exists to enforce (R5.1).

type TokenSourceFunc

type TokenSourceFunc func(ctx context.Context) (string, error)

TokenSourceFunc is the composition-facing closure a direct-mode provider calls on every request to mint a fresh bearer token. The returned string is the access token; the error is ALREADY sanitised (no bearer material) and is safe to surface to a human or a log. It is the ONE seam the registry's bearer RoundTripper holds; tests inject a fake to exercise the transport without a real OS keyring.

func DirectTokenSource

func DirectTokenSource(configPath string, diag port.Diagnostics) (TokenSourceFunc, error)

DirectTokenSource builds the NON-INTERACTIVE token source a direct-mode `toolhive` provider calls on every request. A genuine cache miss (no cached or refreshable refresh token) returns llm.ErrTokenRequired — the caller surfaces it with ErrTokenRequiredHint; it NEVER silently launches a browser from a headless daemon. The returned TokenSourceFunc sanitises every error via llm.SanitizeTokenError so no bearer material an OIDC IdP echoes back in a RetrieveError body ever reaches a log or an error string. Returns an error (never a panicking nil func) when the config cannot be read or the secrets provider is unavailable, so the caller sees an actionable cause at construction time and never holds a nil TokenSourceFunc. What the caller DOES with that error is its own call: newDirectGatewayEntry deliberately keeps building (logging ERROR and installing a token source that returns the cause on every request) rather than failing Build, because an unreachable or unconfigured gateway must never brick startup.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL