toolhivellm

package
v0.0.28 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 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 the `toolhive` provider can talk DIRECTLY to the real gateway_url with no local proxy hop. The actual live-listing HTTP call is made by the protocol-generic internal/adapter/openaicompat.Lister, which this package merely feeds a hardcoded loopback base URL.

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 = "" /* 132-byte string literal not displayed */

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 (today's behaviour), 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 prints the fresh access token to stdout — the `mecatui login` subcommand. It is CLI-ONLY: it 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