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
- Variables
- func HasOperatorProviderDefinitions(conventional, importClaude bool, files []string) (bool, error)
- func IsLoopbackAddr(addr string) bool
- func JoinPromptBody(literal, fileBody string) string
- func NewTextLogger(w io.Writer, level slog.Level, warning string) *slog.Logger
- func OIDCValidator(ctx context.Context, c OIDCConfig) (server.PrincipalValidator, error)
- func ParseLogLevel(value string) (slog.Level, bool)
- func ParseOIDCScopes(raw string) ([]string, error)
- func PrintDefaults(w io.Writer, fs *flag.FlagSet)
- func PrintDefaultsExcluding(w io.Writer, fs *flag.FlagSet, exclude map[string]bool)
- func PrintFlagDefault(w io.Writer, f *flag.Flag)
- func RegisterOIDCFlags(fs *flag.FlagSet, c *OIDCConfig)
- func ResolveMCPAuthority(opts MCPAuthorityOptions) (*mcpauthority.Result, error)
- func ValidateOIDCAuthToken(c OIDCConfig, authToken string) error
- type HeadlessTelemetryConfig
- type HeadlessTelemetryHandles
- type KeyValueList
- type LogLevelFlags
- type MCPAuthorityOptions
- type MCPProfileError
- type MCPProfileLoadOptions
- type MCPProfileResolver
- type MCPProfiles
- type MCPServerList
- type ModelFlagHelp
- type OIDCConfig
- type OIDCProfileProjection
- type ProviderCredentialResolver
- type ProviderFlagHelp
- type ProviderFlags
- func (pf *ProviderFlags) Apply(cfg *app.Config) ResolvedKeys
- func (pf *ProviderFlags) ApplyResolved(cfg *app.Config, keys ResolvedCredentials)
- func (pf *ProviderFlags) ApplyResolvedAPIKeys(cfg *app.Config, keys ResolvedCredentials)
- func (pf *ProviderFlags) AuthFilePath() (path string, explicit bool)
- func (pf *ProviderFlags) EndpointOverrides() permconfig.ProviderOverrides
- func (pf *ProviderFlags) Resolve() ResolvedCredentials
- type ResolvedCredentials
- type ResolvedKeys
- type ToolhiveLLMFlagHelp
- type ToolhiveLLMFlags
Constants ¶
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.
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.
const DefaultMaxJWKSStaleness = time.Hour
DefaultMaxJWKSStaleness is the bounded-by-default key-cache policy (ADR 0205).
Variables ¶
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") )
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.
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).
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.
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 ¶
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 ¶
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 ¶
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
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
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
ParseOIDCScopes parses the shared CLI/Helm scope syntax.
func PrintDefaults ¶ added in v0.0.25
PrintDefaults writes every flag using the conventional CLI spelling: --name for multi-character names and -n for one-character aliases.
func PrintDefaultsExcluding ¶
PrintDefaultsExcluding writes a flag.FlagSet's defaults, skipping any flag whose name is in exclude. It is the ONE filtered-defaults formatter shared by the cmd mains' progressive help renderers, so there is no second hand-rolled PrintDefaults.
It does NOT mutate, rebind, or copy the source FlagSet: it iterates the real flags via VisitAll and formats each in place, so original flag.Value defaults are untouched. A nil exclude set (or an empty map) renders every flag.
The per-flag formatting follows flag.PrintDefaults for the back-quoted/type name from flag.UnquoteUsage, indentation, embedded-newline rewrite, and default-value annotation. It deliberately displays multi-character names as conventional long options (--name), while one-character names remain short options (-n). Long usage strings are word-wrapped to helpWrapWidth so narrow terminals never receive >80-col lines.
func PrintFlagDefault ¶
PrintFlagDefault writes a single flag's usage block in the format of flag.PrintDefaults (possibly word-wrapped). It is the selected/predicate form grouped progressive-help renderers use (a grouped renderer sorts + selects flag names, then calls this per flag), keeping ONE formatter implementation behind both the exhaustive and the grouped paths.
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 ¶
func HeadlessTelemetry(ctx context.Context, cfg HeadlessTelemetryConfig) (HeadlessTelemetryHandles, error)
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 ¶
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.
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:
- 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.
- 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 ¶
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).