cliconfig

package
v0.0.33 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

Documentation

Overview

Package cliconfig holds the small slices of CLI/composition wiring that the four command mains (cmd/mecated, cmd/mecatui, cmd/mecatequi, cmd/mecak8s) would otherwise copy-paste — extracted here so they cannot drift apart. It is a CMD-SIDE composition helper: it may read the process environment (os.Getenv) and register flags (flag.FlagSet), then apply the resolved values onto an app.Config.

Why it lives in internal/ and not in internal/app: app.Build deliberately reads the environment ONLY through its injected envDetector seam (see internal/app/build.go), so the os.Getenv reads for provider credentials belong OUTSIDE app — in the cmd layer or a cmd-side helper like this one. The dependency direction stays inward (cmd -> cliconfig -> app); cliconfig never imports a cmd main. The credentials-FILE parsing itself (auth.yaml) lives one layer further out, in the small leaf adapter internal/adapter/authfile, so a future credential-writing subcommand can depend on that schema directly without pulling in cliconfig's flag/model-alias machinery too; cliconfig only wires it (path resolution + precedence against the environment).

Index

Constants

View Source
const DefaultMCPServerFlagHelp = "remote MCP server as name=URL (repeatable); auth token read from MCP_<NAME>_TOKEN. " +
	"The name must match [A-Za-z0-9_]+ and be case-insensitively unique across entries (it derives the token env var); " +
	"a token-bearing URL must be https, or http to a loopback host (see --mcp-server-insecure-http for the explicit per-server opt-out)"

DefaultMCPServerFlagHelp is the shared --mcp-server help text (the mecated wording plus the ADR-0082 hardening notes). A caller may override it per-main (mirroring the other Register* helpers), but the default keeps the three mains' --help identical.

View Source
const DefaultMCPServerInsecureHTTPFlagHelp = "EXPLICIT PER-SERVER OPT-IN (repeatable): name of a --mcp-server entry whose " +
	"MCP_<NAME>_TOKEN bearer may ride plain http to a NON-loopback host. This acknowledges the token travels CLEARTEXT " +
	"on the network path to that server — you are relying on network-layer controls (NetworkPolicy / namespace trust) " +
	"plus short-lived tokens as the mitigations. It relaxes ONLY the http scheme gate, ONLY for the named server, " +
	"order-independently of where its --mcp-server appears; naming a server that is not registered, or whose URL is " +
	"already https / loopback / non-http, is an error (a stale acknowledgment is loud, never silently inert)"

DefaultMCPServerInsecureHTTPFlagHelp is the shared --mcp-server-insecure-http help text (issue #358, ADR 0090). Unlike --mcp-server it is NOT overridable per-main: the acknowledgment wording is the point of the flag, so all three mains state it identically.

View Source
const DefaultMaxJWKSStaleness = time.Hour

DefaultMaxJWKSStaleness is the bounded-by-default key-cache policy (ADR 0205).

Variables

View Source
var (
	// ErrMCPProfileInvalid identifies invalid resolved profile metadata.
	ErrMCPProfileInvalid = errors.New("invalid MCP profile")
	// ErrMCPProfileSecret identifies a missing or malformed referenced secret.
	ErrMCPProfileSecret = errors.New("MCP profile secret unavailable")
	// ErrMCPProfileStore identifies a credential source that could not be opened.
	ErrMCPProfileStore = errors.New("MCP credential source unavailable")
)
View Source
var DefaultModelFlagHelp = ModelFlagHelp{
	ModelAlias: "model alias mapping as name=model-id (repeatable), e.g. --model-alias fast=gpt-4o-mini. Aliases are resolved only in the composition layer; an agent def's `model: <alias>` resolves through this map (then the built-in sonnet/opus/haiku aliases)",
	ModelSlot:  "per-slot model binding as slot=selector (repeatable), e.g. --model-slot compaction=cheap --model-slot cheap=gpt-4o-mini (ADR 0030). A SLOT routes an internal lightweight LLM call to its own model: the wired slots are `compaction` (the compaction summary call), `ask-reviewer` (the headless child-ask reviewer), and `guardrail` (the content checker); a TIER key (`cheap`/`fast`/`reasoning`) gives a default a slot falls through to (each routed slot defaults to `cheap`). The selector is an alias (resolved through --model-alias / the built-ins) or a concrete id. Empty (no --model-slot) keeps every call on the session model (byte-identical default). FAIL-SOFT: a typo'd slot or an alias meaning inherit WARNs and keeps the session model. For ask-reviewer/guardrail the slot supersedes the model of --subagent-ask-reviewer/--guardrails-model but does NOT enable them (those flags stay the on/off gate). Operator-tier only; the YAML twin is the user-global settings.yaml `models.slots:` subtree",
}

DefaultModelFlagHelp is the mecated-style wording, used when a field of the passed ModelFlagHelp is empty. It mirrors the help text the mecated twin carried before the extraction.

View Source
var DefaultProviderFlagHelp = ProviderFlagHelp{
	OpenAIBaseURL:     "override the OpenAI API base URL (compatible endpoints)",
	OpenRouterBaseURL: "override the OpenRouter API base URL (default https://openrouter.ai/api/v1; key from OPENROUTER_API_KEY)",
	AnthropicBaseURL:  "override the native Anthropic API base URL (compatible/proxy endpoints; key from ANTHROPIC_API_KEY)",
	OpenCodeBaseURL:   "override the OpenCode Go API base URL (default https://opencode.ai/zen/go/v1; key from OPENCODE_API_KEY)",
	AuthFile:          "provider credentials YAML path (default: $XDG_CONFIG_HOME/mecatl/auth.yaml); environment credentials take precedence",
}

DefaultProviderFlagHelp is the mecated-style wording, used when a field of the passed ProviderFlagHelp is empty. It is the right default for a fresh consumer (mecatequi).

View Source
var DefaultToolhiveLLMFlagHelp = ToolhiveLLMFlagHelp{
	Enable: "auto-detect a locally-running ToolHive LLM proxy by reading ToolHive's config and probing " +
		"127.0.0.1, and register it as a model provider (id \"toolhive\", no API key needed); unrelated to " +
		"--toolhive (MCP workload discovery). Set =false on shared hosts",
	BaseURL: "explicit ToolHive LLM proxy base URL (must resolve to loopback); skips the config-file " +
		"auto-detect but keeps the startup probe",
	Mode: "ToolHive LLM routing mode: \"auto\" (default; direct when the OIDC trio is configured, else the " +
		"loopback proxy), \"proxy\" (force the loopback reverse proxy), or \"direct\" (talk to the real " +
		"gateway_url with an in-process OIDC token; fails when OIDC is not configured). The explicit " +
		"--toolhive-llm-base-url override is always proxy mode",
}

DefaultToolhiveLLMFlagHelp is the shared wording every consumer starts from. It explicitly disambiguates from the UNRELATED --toolhive flag (ToolHive MCP workload discovery) — the two features share a vendor name and nothing else, and issue #262's own review flagged the naming collision as the #1 confusion risk.

View Source
var ErrOIDCMisconfigured = errors.New("oidc: misconfigured")

ErrOIDCMisconfigured is returned when caller identity or its protected resource profile is requested but cannot be wired. It is fatal at startup by design.

Functions

func HasOperatorProviderDefinitions

func HasOperatorProviderDefinitions(conventional, importClaude bool, files []string) (bool, error)

HasOperatorProviderDefinitions reports whether the operator settings declare at least one custom provider. It is an embedded-server preflight only; app.Build remains the sole owner of the resolved definitions used for construction.

func IsLoopbackAddr

func IsLoopbackAddr(addr string) bool

IsLoopbackAddr reports whether addr is a loopback bind: the host is the literal "localhost" or a loopback IP (127.0.0.0/8, ::1). It is the single fail-closed gate (ADR 0018 decision 6) the four mains share for binding the UNAUTHENTICATED admin/metrics surface — the admin mux output is secret-shaped (pprof/expvar/ metrics can embed prompt text, file paths, goroutine stacks), so a non-loopback bind is rejected at parse time.

A malformed address (no port) is treated as the bare host; an empty or unparseable host is NOT loopback (fail safe).

ACCEPTED assumption (security review Low): the literal string "localhost" is trusted as loopback without resolving it. A self-inflicted /etc/hosts override is contrived and single-user; the SDK's DNS-rebinding/Host validation remains the runtime backstop.

func JoinPromptBody

func JoinPromptBody(literal, fileBody string) string

JoinPromptBody concatenates a --prompt literal and a --prompt-file body into the single prompt text a main hands onward. Both may be supplied; the literal comes first, separated by a blank line. Each side is included only when non-empty so a lone source never carries a stray separator, and trailing newlines are trimmed so a file that ends in a newline joins identically to one that does not.

Shared because both prompt-bearing mains must agree byte-for-byte on the join: cmd/mecatequi (the headless one-shot, which then layers its trusted-instructions / untrusted-fence wrapping on top) and cmd/mecatui (the interactive seed prompt). They had independent identical copies; only one was tested.

func NewTextLogger added in v0.0.23

func NewTextLogger(w io.Writer, level slog.Level, warning string) *slog.Logger

NewTextLogger constructs the text logger used by command roots. It deliberately does not install a default logger; cmd/ owns that process-global operation. warning, when non-empty, is emitted after the logger is configured.

func OIDCValidator

func OIDCValidator(ctx context.Context, c OIDCConfig) (server.PrincipalValidator, error)

OIDCValidator resolves c into a token validator, or (nil, nil) when caller identity is off (the unchanged path).

ctx MUST be the SERVER-ROOT context: the validator owns background JWKS refresh, so binding it to a per-request context would tear key rotation down with the first request.

Every failure is FATAL — the caller must refuse to start. Degrading to the unauthenticated path here would silently turn an authenticated deployment into an open one.

func ParseLogLevel added in v0.0.23

func ParseLogLevel(value string) (slog.Level, bool)

ParseLogLevel maps the exact supported command-line tokens to slog levels. The boolean is false for every other token, including the empty string.

func ParseOIDCScopes added in v0.0.26

func ParseOIDCScopes(raw string) ([]string, error)

ParseOIDCScopes parses the shared CLI/Helm scope syntax.

func RegisterOIDCFlags

func RegisterOIDCFlags(fs *flag.FlagSet, c *OIDCConfig)

RegisterOIDCFlags registers the caller-identity flags on fs. Both server mains call it so the names and help text are identical.

func ResolveMCPAuthority added in v0.0.26

func ResolveMCPAuthority(opts MCPAuthorityOptions) (*mcpauthority.Result, error)

ResolveMCPAuthority applies exactly one mode-specific validation and loading path. Broker mode retains neutral declarations and opens no global resources.

func ValidateOIDCAuthToken

func ValidateOIDCAuthToken(c OIDCConfig, authToken string) error

ValidateOIDCAuthToken rejects two incompatible edge-authentication modes. An opaque static bearer cannot also be the OIDC JWT that caller identity validates. Keeping this in shared config makes mecated and mecak8s fail identically.

Types

type HeadlessTelemetryConfig

type HeadlessTelemetryConfig struct {
	// ServiceName sets the resource service.name attribute. Defaults to "mecatl"
	// when empty (matches telemetry.Setup).
	ServiceName string

	// OTLPTraceEndpoint enables OTLP TRACE push when non-empty. Mirrors mecated's
	// --otlp-endpoint. Empty disables tracing.
	OTLPTraceEndpoint string
	// OTLPTraceProtocol selects the trace transport: "grpc" (default) or "http".
	OTLPTraceProtocol string
	// OTLPTraceInsecure skips TLS when dialing the trace collector (dev only).
	OTLPTraceInsecure bool

	// OTLPMetricsEndpoint enables OTLP METRICS push when non-empty. The prometheus
	// reader stays always on regardless (see telemetry.Setup). Empty installs no
	// periodic reader.
	OTLPMetricsEndpoint string
	// OTLPMetricsProtocol selects the metrics transport: "grpc" (default) or "http".
	OTLPMetricsProtocol string
	// OTLPMetricsInsecure skips TLS when dialing the metrics collector (dev only).
	OTLPMetricsInsecure bool

	// Scrape, when true, builds the pipeline (Setup + Metrics + the role="main"
	// EventSink + the child scoper) even when no OTLP endpoint is set — the
	// scrape-only deployment (mecak8s --metrics-addr with no --otlp-*). The
	// prometheus reader is always on, so the returned Registry serves /metrics.
	// Tracing stays a no-op when no trace endpoint is set. False (the default)
	// keeps the byte-identical no-op posture when no endpoints are configured.
	Scrape bool
}

HeadlessTelemetryConfig carries the OTLP endpoint knobs the headless mains expose on their flags. A zero value (every Endpoint empty) produces zero handles — the byte-identical no-telemetry posture.

type HeadlessTelemetryHandles

type HeadlessTelemetryHandles struct {
	// Shutdown flushes + stops the providers (metrics periodic reader + trace
	// batch processor). Always non-nil (a no-op when telemetry is disabled), so a
	// caller can defer it unconditionally.
	Shutdown func(context.Context) error
	// Registry is the prometheus registry the metrics exporter registers on;
	// serve it via telemetry.NewAdminMux at /metrics (mecak8s). Nil when telemetry
	// is disabled.
	Registry *prometheus.Registry
	// Metrics is the domain metrics adapter; nil when disabled.
	Metrics *telemetry.Metrics
	// Sink is the fanned-out EventSink (metrics + tracing) tagged role="main";
	// nil when disabled.
	Sink port.EventSink
	// ToolCallRecorder is the role="main" tool-call recorder; nil when disabled.
	ToolCallRecorder port.ToolCallRecorder
	// MetricsRoleScoper is the closure handing each CHILD engine a role-scoped
	// (EventSink, ToolCallRecorder) pair keyed on the BOUNDED family label
	// internal/app's roleFamily already resolved; nil when disabled (children
	// unmetered, byte-identical).
	MetricsRoleScoper func(familyRole string) (port.EventSink, port.ToolCallRecorder)
	// SessionLoadFailureMetricsEmitter records ownership-concealed load failures
	// with one closed class label. Nil when telemetry is disabled.
	SessionLoadFailureMetricsEmitter func(port.SessionLoadFailureClass)
}

HeadlessTelemetryHandles bundles the handles HeadlessTelemetry returns. A caller threads Sink/ToolCallRecorder/MetricsRoleScoper into app.Config and owns the Shutdown defer (a bounded ctx so a dead collector cannot hang exit). Every field is zero-valued when telemetry is disabled, so a caller can pass them straight into app.Config without nil-checking (nil Sink/ToolCallRecorder/ MetricsRoleScoper is the byte-identical no-metrics path).

func HeadlessTelemetry

HeadlessTelemetry builds the OTel metrics + (optional) tracing pipeline for a headless main and returns the handles to thread into app.Config. When both the trace and metrics endpoints are empty it returns zero handles and a no-op Shutdown — the byte-identical no-telemetry posture, so a headless main with no --otlp-* flags is unchanged.

The caller owns the Shutdown defer; mecatequi registers it BEFORE built.Close() so the flush runs first (defers are LIFO), and mecak8s registers it so the SIGTERM path flushes OTLP before the listener stops.

type KeyValueList

type KeyValueList map[string]string

KeyValueList is a repeatable "key=value" flag.Value collecting into a last-write-wins map. It backs --model-alias (e.g. --model-alias fast=gpt-4o-mini --model-alias smart=gpt-5) and --model-slot (e.g. --model-slot compaction=cheap) in both cmd/mecated and cmd/mecatui. Extracted here (issue #93) so the two mains cannot drift apart — the twin copies already had divergent Set error messages in #87.

A nil *KeyValueList is usable: Set lazily allocates the backing map, exactly as the pre-extraction inline types did, so a config struct field's zero value (a nil map) is a valid flag binding.

func RegisterModelFlags

func RegisterModelFlags(fs *flag.FlagSet, help ModelFlagHelp) (aliases, slots *KeyValueList)

RegisterModelFlags registers --model-alias / --model-slot on fs, each bound to its own *KeyValueList, and returns the pair so the caller can thread them onto app.Config (ModelAliases / ModelSlots). The help text comes from help, falling back per-field to DefaultModelFlagHelp so a caller may pass a zero value (or override only the fields it words differently). It is the ONE place the two repeatable model flags are wired, so the two mains (and a future mecatequi consumer) cannot drift apart.

func (*KeyValueList) AsMap

func (m *KeyValueList) AsMap() map[string]string

AsMap returns the backing map as a plain map[string]string (the type app.Config.ModelAliases / ModelSlots expects), or nil for a nil receiver so an unset flag yields the byte-identical default (a nil map, not an empty one). It is the ONE conversion a cmd main does to thread a parsed KeyValueList onto app.Config.

func (*KeyValueList) Set

func (m *KeyValueList) Set(v string) error

Set implements flag.Value. A later occurrence of the same key overrides an earlier one. A value without '=' (or with an empty key) is a parse error.

func (KeyValueList) String

func (m KeyValueList) String() string

String implements flag.Value. It renders the map as a sorted, comma-separated list of key=value pairs (empty for a nil/empty map), the stable form flag's default-value display expects.

type LogLevelFlags added in v0.0.23

type LogLevelFlags struct {
	// contains filtered or unexported fields
}

LogLevelFlags is the shared binding for the command roots' --log-level flag. The roots own the logger and install it after parsing; this helper only parses the small, deliberately closed level vocabulary.

func RegisterLogLevelFlag added in v0.0.23

func RegisterLogLevelFlag(fs *flag.FlagSet) *LogLevelFlags

RegisterLogLevelFlag adds --log-level to fs. Invalid values are intentionally accepted so a typo cannot make a daemon fail to start; Resolve reports a warning for the root to emit after it has installed the configured logger.

func (*LogLevelFlags) Resolve added in v0.0.23

func (f *LogLevelFlags) Resolve() (slog.Level, string)

Resolve returns the configured slog level and, for an invalid value, the one warning the command root should emit. Matching is exact and case-sensitive.

type MCPAuthorityOptions added in v0.0.26

type MCPAuthorityOptions struct {
	Operator        *permconfig.MCPSection
	Legacy          *MCPServerList
	LookupEnv       func(string) (string, bool)
	DefaultMode     mcpauthority.Mode
	BrokerSupported bool
}

MCPAuthorityOptions supplies the already-parsed operator block and command-root policy.

type MCPProfileError

type MCPProfileError struct {
	Server   string
	Field    string
	Ref      string
	Expected string
	Remedy   string
	Kind     error
}

MCPProfileError is a redacted profile-loading error. It retains only safe metadata and a stable category; looked-up values and adapter causes are never retained.

func (*MCPProfileError) Error

func (e *MCPProfileError) Error() string

func (*MCPProfileError) Is

func (e *MCPProfileError) Is(target error) bool

Is reports whether target is this error's stable safe category.

type MCPProfileLoadOptions

type MCPProfileLoadOptions struct {
	Operator  *permconfig.MCPSection
	Legacy    *MCPServerList
	LookupEnv func(string) (string, bool)
}

MCPProfileLoadOptions are explicit metadata and environment inputs for the canonical runtime profile loader.

type MCPProfileResolver

type MCPProfileResolver struct {
	// contains filtered or unexported fields
}

MCPProfileResolver binds the legacy CLI metadata and environment lookup to the canonical profile loader. app.Build supplies the operator subtree from its already-created settings resolver, avoiding a second YAML parse.

func NewMCPProfileResolver

func NewMCPProfileResolver(legacy *MCPServerList, lookup func(string) (string, bool)) *MCPProfileResolver

NewMCPProfileResolver constructs a side-effect-free resolver. Environment and credential stores are touched only when Load is called by composition.

func (*MCPProfileResolver) Load

func (r *MCPProfileResolver) Load(operator *permconfig.MCPSection) ([]mcp.ServerConfig, interface{ Close() error }, error)

Load implements app.Config's MCP profile-loader capability without importing the composition package.

func (*MCPProfileResolver) LoadAuthority added in v0.0.26

func (r *MCPProfileResolver) LoadAuthority(operator *permconfig.MCPSection, defaultMode mcpauthority.Mode, brokerSupported bool) (*mcpauthority.Result, error)

LoadAuthority resolves exactly one authority path without reparsing settings or constructing broker runtime resources.

type MCPProfiles

type MCPProfiles struct {
	Servers []mcp.ServerConfig
	// contains filtered or unexported fields
}

MCPProfiles owns the credential stores/readers backing Servers. Close it only after every manager/controller using those servers has closed.

func LoadMCPProfiles

func LoadMCPProfiles(opts MCPProfileLoadOptions) (*MCPProfiles, error)

LoadMCPProfiles resolves the selected operator and legacy entries. Settings retain their order; a same-name legacy CLI entry replaces the whole settings entry in place, and a distinct legacy entry appends.

func (*MCPProfiles) Close

func (p *MCPProfiles) Close() error

Close closes each loader-created source exactly once.

func (*MCPProfiles) OAuthServer

func (p *MCPProfiles) OAuthServer(name string) (mcp.ServerConfig, bool)

OAuthServer selects a configured OAuth server case-insensitively. The returned value borrows its credential source from p and is suitable for the explicit login path while p remains open.

type MCPServerList

type MCPServerList struct {
	// contains filtered or unexported fields
}

MCPServerList is a repeatable flag.Value collecting --mcp-server name=URL entries into mcp.ServerConfig values, plus the --mcp-server-insecure-http relaxation names its Finalize step resolves against them. It was extracted from cmd/mecated (issue #341) so mecatequi and mecak8s register the SAME flag + token convention instead of growing three drifting copies: a scheduler (titlani) launching one-shot runs injects a short-lived per-run identity as MCP_<NAME>_TOKEN, and the run presents it as a Bearer to the named MCP endpoint.

The per-server token env read (MCP_<NAME>_TOKEN, name upper-cased) happens only in Finalize or LoadMCPProfiles. Set and flag help retain metadata only, so parse/inspection paths are lookup-free. The token is SECRET-shaped and is never logged. A missing/empty token simply leaves Headers nil (token optional — an unauthenticated dev endpoint stays reachable).

LIFECYCLE (issues #358/#523): Set only COLLECTS; runtime resolution and the token-bearing scheme gate (CWE-319) run in Finalize, which every existing main calls right after flag.Parse. Deferring the gate is what makes --mcp-server-insecure-http order-independent — a relaxation parsed after its --mcp-server would otherwise arrive too late. Servers() refuses (panics) before a successful Finalize, so a main cannot hand un-gated configs to app.Build by forgetting the call.

func RegisterMCPServerFlag

func RegisterMCPServerFlag(fs *flag.FlagSet, help string) *MCPServerList

RegisterMCPServerFlag registers --mcp-server AND its companion --mcp-server-insecure-http on fs, bound to one fresh MCPServerList, and returns it, so a cmd main threads the parsed servers onto app.Config via Servers() after calling Finalize post-parse. It is the ONE registration path for both flags — mecated, mecatequi, and mecak8s all use it, so the flag names, parse semantics, the MCP_<NAME>_TOKEN convention, and the insecure-http acknowledgment wording cannot drift apart. An empty help falls back to DefaultMCPServerFlagHelp (the mecated wording); the insecure-http help is deliberately NOT overridable.

func (*MCPServerList) Finalize

func (l *MCPServerList) Finalize() error

Finalize is the post-parse validation step every main calls right after flag.Parse (issue #358). It resolves the --mcp-server-insecure-http relaxations against the collected --mcp-server entries and THEN runs the token-bearing scheme gate, so the two flags compose order-independently:

  1. Every relaxation must name a registered server (case-insensitively, matching the env-var derivation) whose URL actually IS plain http to a non-loopback host. Anything else — an unknown name, an https server, a loopback server, a non-http scheme — is an error: a stale acknowledgment must be loud, never silently inert.
  2. Every token-bearing entry NOT relaxed must pass mcp.ValidateClientURL (https, or http to loopback) — the unchanged CWE-319 default posture.

Nil-receiver safe (a config built without RegisterMCPServerFlag has nothing to gate). Idempotent on success.

func (*MCPServerList) Servers

func (l *MCPServerList) Servers() []mcp.ServerConfig

Servers returns the collected configs as the plain []mcp.ServerConfig app.Config.MCPServers expects, or nil for a nil receiver so a config struct built WITHOUT RegisterMCPServerFlag (e.g. a test constructing the cmd config directly) yields the byte-identical unset default — mirroring KeyValueList.AsMap's nil-receiver discipline.

FAIL-CLOSED: a non-nil list panics if Finalize has not succeeded — the deferred token-bearing scheme gate (see Finalize) has not run, and handing out un-gated configs would silently reopen CWE-319. The panic is a programmer-error guard for a future main that forgets the post-parse call, never an operator-reachable path (all three mains Finalize in parseFlags).

func (*MCPServerList) Set

func (l *MCPServerList) Set(v string) error

Set parses a single "name=URL" entry, splitting at the FIRST '=' (a URL may itself contain '='). It records the derived MCP_<NAME>_TOKEN reference but deliberately does not read it; Finalize or LoadMCPProfiles performs runtime secret resolution after command selection.

Two ADR-0082 hardenings (applied to all three mains, deliberately tightening mecated's original behavior):

  • CWE-178: the name must match mcpServerName, and two entries whose upper-cased names collide (vmcp/VMCP/vMcp -> MCP_VMCP_TOKEN) are rejected — otherwise the second server would silently share (or steal) the first one's token. Both checks are entry-local, so they stay inline.
  • CWE-319: when a token IS attached, the URL must be https — or http to an explicit loopback host — so a scheduler-injected bearer is never sent cleartext off-host. Since issue #358 that gate runs in Finalize, NOT here: it must see the full --mcp-server-insecure-http relaxation set, which argv may order after this entry. A tokenless URL is not gated (unchanged).

func (*MCPServerList) String

func (l *MCPServerList) String() string

String implements flag.Value: the comma-joined server NAMES only (never a URL query secret or a header), the stable form flag's default display expects.

func (*MCPServerList) Validate

func (l *MCPServerList) Validate() error

Validate checks only cross-flag metadata. It performs no environment lookup; runtime secret resolution belongs to LoadMCPProfiles.

type ModelFlagHelp

type ModelFlagHelp struct {
	ModelAlias string
	ModelSlot  string
}

ModelFlagHelp carries the per-main help text for the two repeatable model flags --model-alias and --model-slot. The two mains word these slightly differently (mecatui prefixes "embedded server only:"), so the help is passed in rather than hard-coded — keeping each main's --help BYTE-IDENTICAL across the extraction. A zero ModelFlagHelp falls back to DefaultModelFlagHelp (the mecated wording), which is what a new consumer (mecatequi) would use.

type OIDCConfig

type OIDCConfig struct {
	// Issuer is the IdP that mints the tokens (the `iss` claim, byte-exact). It
	// is the ON switch: empty means caller identity is off.
	Issuer string
	// JWKSURI, when set, is the STATIC signing-key endpoint; it short-circuits
	// OIDC discovery (the offline-test and air-gap hook).
	JWKSURI string
	// Audience is the `aud` this deployment accepts. REQUIRED when Issuer is
	// set: an audience-less verifier accepts tokens minted for other services.
	Audience string

	// Resource and ClientID form the optional RFC 9728 protected-resource
	// profile. Issuer and Audience remain the sole authoritative identity values.
	Resource string
	ClientID string

	// ScopesCSV is the operator-facing CSV spelling. Scopes is the validated,
	// deterministic metadata representation populated by ValidateOIDCProfile.
	ScopesCSV string
	Scopes    []string

	// MaxJWKSStaleness bounds how long cached signing keys remain trusted when
	// refresh cannot reach the IdP. Zero explicitly disables the upper bound.
	MaxJWKSStaleness time.Duration

	// NewValidator constructs the token validator. Nil selects the production
	// toolhive-core/authn adapter. Tests may replace it to observe construction or
	// force a startup failure; it is not a deployment extension point.
	NewValidator func(ctx context.Context, c OIDCConfig) (server.PrincipalValidator, error)

	// InsecureAllowPrivateIssuer is the deprecated legacy escape hatch. It permits
	// BOTH HTTP and private issuer/JWKS addresses; use AllowPrivateHTTPSIssuer
	// with TrustedCAFile for a private HTTPS issuer.
	InsecureAllowPrivateIssuer bool
	// AllowPrivateHTTPSIssuer permits only private-address admission for an HTTPS
	// issuer/JWKS endpoint. It requires TrustedCAFile and retains TLS, hostname,
	// redirect, and DNS-pinned dialing protections.
	AllowPrivateHTTPSIssuer bool
	// TrustedCAFile is the PEM CA bundle required by AllowPrivateHTTPSIssuer.
	TrustedCAFile string
	// contains filtered or unexported fields
}

OIDCConfig carries the caller-identity flags. The zero value is identity OFF — the byte-identical no-auth posture.

func (OIDCConfig) Enabled

func (c OIDCConfig) Enabled() bool

Enabled reports whether the operator asked for caller identity.

func (OIDCConfig) InsecureIssuerWarning

func (c OIDCConfig) InsecureIssuerWarning() string

InsecureIssuerWarning returns the operator-facing warning for a configuration that has relaxed the issuer-URL and private-address checks, or "" when it has not.

It is a returned STRING rather than a log call because cliconfig owns no logger — the same shape as ResolvedKeys.AuthFileWarning, which the cmd/ mains surface with slog.Warn. Both server mains log this one too.

Silence here is how a test flag becomes a production vulnerability: an operator who copy-pastes it out of a test fixture gets no other signal that they have switched off an SSRF defence.

func (*OIDCConfig) ProfileProjection added in v0.0.26

func (c *OIDCConfig) ProfileProjection() (OIDCProfileProjection, error)

ProfileProjection returns the validated metadata projection.

func (OIDCConfig) ProtectedResourceEnabled added in v0.0.26

func (c OIDCConfig) ProtectedResourceEnabled() bool

ProtectedResourceEnabled reports whether both profile identity fields are present. ValidateOIDCProfile must still be called before serving.

func (*OIDCConfig) ValidateOIDCProfile added in v0.0.26

func (c *OIDCConfig) ValidateOIDCProfile() error

ValidateOIDCProfile validates the optional profile before listeners start.

type OIDCProfileProjection added in v0.0.26

type OIDCProfileProjection struct {
	Resource string
	ClientID string
	Issuer   string
	Audience string
	Scopes   []string
}

OIDCProfileProjection is the validated, operator-controlled metadata input. Issuer and Audience are copied from the same OIDCConfig used by the token validator; there is no second issuer or audience policy.

func (OIDCProfileProjection) ProtectedResourceProfile added in v0.0.26

func (p OIDCProfileProjection) ProtectedResourceProfile() server.ProtectedResourceProfile

ProtectedResourceProfile converts this validated projection to the public metadata shape. It deliberately exposes only fields RFC 9728 permits here.

type ProviderCredentialResolver

type ProviderCredentialResolver struct {
	// contains filtered or unexported fields
}

ProviderCredentialResolver adapts the command root's one immutable credential snapshot to app.Build's composition-owned provider-credential seam.

func NewProviderCredentialResolver

func NewProviderCredentialResolver(flags *ProviderFlags, keys ResolvedCredentials) *ProviderCredentialResolver

NewProviderCredentialResolver constructs a loader over an already-resolved command snapshot. Load performs no filesystem or environment I/O.

func (*ProviderCredentialResolver) Load

func (r *ProviderCredentialResolver) Load(definitions permconfig.ProviderDefinitions) (app.ProviderCredentials, interface{ Close() error }, error)

Load validates custom auth IDs only after Build has resolved the operator definitions, then returns a detached immutable-by-convention credential snapshot.

type ProviderFlagHelp

type ProviderFlagHelp struct {
	OpenAIBaseURL     string
	OpenRouterBaseURL string
	AnthropicBaseURL  string
	OpenCodeBaseURL   string
	AuthFile          string
}

ProviderFlagHelp carries the per-main help text for the three provider base-URL flags. The three mains word these slightly differently (mecated is the daemon; mecatui prefixes "embedded server only:"), so the help is passed in rather than hard-coded — keeping each main's --help BYTE-IDENTICAL across the extraction. A zero ProviderFlagHelp falls back to DefaultProviderFlagHelp (the mecated wording), which is what a new consumer (mecatequi) uses.

type ProviderFlags

type ProviderFlags struct {
	// contains filtered or unexported fields
}

ProviderFlags holds the values bound by RegisterProviderFlags. The base-URL fields are populated by flag parsing; the key fields are populated by Apply (read from the environment, then from an auth.yaml credentials file) so a key never has to round-trip through the process argv. It is the ONE place the three provider credentials + base URLs are wired onto app.Config, so the six fields can never again be partially wired (the bug that left mecatequi unable to reach Anthropic / OpenRouter).

func RegisterProviderFlags

func RegisterProviderFlags(fs *flag.FlagSet, help ProviderFlagHelp) *ProviderFlags

RegisterProviderFlags registers --openai-base-url / --openrouter-base-url / --anthropic-base-url / --auth-file on fs and returns the binding to pass to Apply later. The help text comes from help, falling back per-field to DefaultProviderFlagHelp so a caller may pass a zero value (or override only the fields it words differently).

func (*ProviderFlags) Apply

func (pf *ProviderFlags) Apply(cfg *app.Config) ResolvedKeys

Apply is the compatibility wrapper that resolves the four API-key credentials plus the file-only Codex credential, then projects the resulting snapshot. Production roots instead call Resolve once and ApplyResolved wherever the resulting app.Config is assembled. A non-empty AuthFileWarning is safe for a root to surface once; credential values must never be logged.

func (*ProviderFlags) ApplyResolved

func (pf *ProviderFlags) ApplyResolved(cfg *app.Config, keys ResolvedCredentials)

ApplyResolved projects a previously resolved snapshot without touching the environment or filesystem. This is the production command-root seam.

func (*ProviderFlags) ApplyResolvedAPIKeys

func (pf *ProviderFlags) ApplyResolvedAPIKeys(cfg *app.Config, keys ResolvedCredentials)

ApplyResolvedAPIKeys is the explicit projection for a command root which does not support the manual Codex credential (currently mecak8s).

func (*ProviderFlags) AuthFilePath

func (pf *ProviderFlags) AuthFilePath() (path string, explicit bool)

AuthFilePath reports the path Resolve would inspect and whether it came from --auth-file. It exposes path provenance without exposing credentials so a command-specific startup policy can decide how to present a conventional missing-file result.

func (*ProviderFlags) EndpointOverrides

func (pf *ProviderFlags) EndpointOverrides() permconfig.ProviderOverrides

EndpointOverrides returns the non-secret CLI endpoint overrides. Command roots map this directly onto app.Config; Build merges it over settings-derived overrides.

func (*ProviderFlags) Resolve

func (pf *ProviderFlags) Resolve() ResolvedCredentials

Resolve reads every ambient API-key input and auth.yaml exactly once and returns the immutable credential snapshot command roots cache for their lifetime. The manual Codex token intentionally has no environment seam.

type ResolvedCredentials

type ResolvedCredentials struct {
	OpenAI     string
	OpenRouter string
	Anthropic  string
	OpenCode   string
	// OpenAICodex is a distinct billing identity from OpenAIKey. Its fields are
	// immutable outside the provider adjunct and it is populated only after
	// startup validation of a file-backed manual token.
	OpenAICodex openaicodex.Credential

	// AuthFileWarning is non-empty when the auth.yaml credentials file (the explicit
	// --auth-file path, or the conventional default) could not be read or parsed
	// cleanly. It is set by Resolve/Apply (ReadProviderKeys alone never touches the file).
	// Never fatal — Apply always falls back to whatever was resolved from the
	// environment — but a caller should log it (cmd/ mains: slog.Warn) so a typo in
	// auth.yaml doesn't fail silently. Not secret-shaped: it names the file path and the
	// problem, never a key value.
	AuthFileWarning string
	// contains filtered or unexported fields
}

ResolvedCredentials is the immutable-by-value snapshot resolved from the environment and auth.yaml. The four API-key fields are SECRET-shaped: callers must not log or print them.

func ResolveProviderCredentials

func ResolveProviderCredentials(pf *ProviderFlags, definitions permconfig.ProviderDefinitions, env xdgconfig.ResolveEnv) (ResolvedCredentials, error)

ResolveProviderCredentials resolves the one immutable credential snapshot for a resolved operator provider definition set. Custom credentials come only from auth.yaml; they deliberately have no environment fallback.

func (ResolvedCredentials) Any

func (k ResolvedCredentials) Any() bool

Any reports whether at least one provider credential is present. It is the shared "is any real provider configured?" predicate (mecatui uses it for its startup guard).

func (ResolvedCredentials) CustomAPIKey

func (k ResolvedCredentials) CustomAPIKey(id string) string

CustomAPIKey returns the file-only key for one custom provider.

func (ResolvedCredentials) CustomAvailable

func (k ResolvedCredentials) CustomAvailable(id string) bool

CustomAvailable reports whether the custom provider has the authentication its validated definition requires.

func (ResolvedCredentials) HasOpenAICodex

func (k ResolvedCredentials) HasOpenAICodex() bool

HasOpenAICodex reports whether validation produced a usable manual token.

type ResolvedKeys

type ResolvedKeys = ResolvedCredentials

ResolvedKeys remains as the source-compatible name for callers that only used the original API-key snapshot.

func ReadProviderKeys

func ReadProviderKeys() ResolvedKeys

ReadProviderKeys reads provider credentials from the environment alone (no auth.yaml). It is the SINGLE definition of which env vars hold which credential. A caller that must account for auth.yaml should call ProviderFlags.Resolve instead. The values are SECRET-shaped; callers must not log or print them.

type ToolhiveLLMFlagHelp

type ToolhiveLLMFlagHelp struct {
	Enable  string
	BaseURL string
	// Mode is the per-main help for --toolhive-llm-mode (issue #265).
	Mode string
}

ToolhiveLLMFlagHelp carries the per-main help text for the two ToolHive LLM gateway flags (issue #262). mecatui prefixes "embedded server only:" (it only matters when mecatui hosts its OWN in-process server); the other three mains use DefaultToolhiveLLMFlagHelp verbatim.

type ToolhiveLLMFlags

type ToolhiveLLMFlags struct {
	// contains filtered or unexported fields
}

ToolhiveLLMFlags holds the values bound by RegisterToolhiveLLMFlags.

func RegisterToolhiveLLMFlags

func RegisterToolhiveLLMFlags(fs *flag.FlagSet, help ToolhiveLLMFlagHelp) *ToolhiveLLMFlags

RegisterToolhiveLLMFlags registers --toolhive-llm (default true), --toolhive-llm-base-url (default ""), and --toolhive-llm-mode (default "auto") on fs. A zero ToolhiveLLMFlagHelp field falls back to DefaultToolhiveLLMFlagHelp, mirroring RegisterProviderFlags.

func (*ToolhiveLLMFlags) Apply

func (tf *ToolhiveLLMFlags) Apply(cfg *app.Config)

Apply writes the three resolved values onto cfg. A nil receiver (a config built WITHOUT RegisterToolhiveLLMFlags — e.g. a test that constructs the cmd config struct directly) leaves the app.Config fields at their zero value (ToolhiveLLM=false, ToolhiveLLMBaseURL="", ToolhiveLLMMode=""), so app.Config's byte-identical-when-unset invariant holds for a caller that never wires this flag set (ToolhiveLLMMode="" resolves to "auto" in resolveToolhiveIntent, the pre-#265 default).

Jump to

Keyboard shortcuts

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