app

package
v0.8.0 Latest Latest
Warning

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

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

Documentation

Overview

Package app holds the dependency-injection builders and config predicates that assemble the RunLore agent. They live here (instead of in cmd/lore behind main()) so the wiring that decides what ships is unit-testable.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildApprovals

func BuildApprovals(cfg *config.Config, exec action.Executor, aud audit.Auditor, log *slog.Logger) *action.Approvals

BuildApprovals enables rung-2 approval-gated execution for action mode "approve" (requires a reachable cluster).

func BuildAuditor

func BuildAuditor(cfg *config.Config, log *slog.Logger) (audit.Auditor, func(), error)

BuildAuditor opens the append-only action audit log when configured, else a no-op. Validate already requires AuditLogPath for both executing modes (approve and auto), so the Nop fallback below only ever applies to off/suggest.

The existing hash chain is verified on open (OpenVerified). If it is broken (insertion, edit, or mid-chain deletion) the response is mode-gated, because this is where cfg is available:

  • approve/auto (RunLore executes + audits actions) → return an error so startup FAILS CLOSED: it must not append to, or act against, a chain whose integrity can no longer be vouched for.
  • off/suggest (nothing is executed) → log a WARNING and proceed: the auditor still opens and keeps appending, so a non-executing deployment isn't blocked.

An empty or absent chain is valid (not broken). Tail-truncation (dropping the most-recent records) leaves a valid prefix and is NOT detectable here — see docs/security-model.md.

func BuildAuto

func BuildAuto(cfg *config.Config, exec action.Executor, aud audit.Auditor, log *slog.Logger) *action.Auto

BuildAuto enables rung-3 unattended execution for action mode "auto" (requires a reachable cluster). Heavily gated: reversible-only, confidence-floored, rate- limited, kill-switchable, and audited. The rung is EXPERIMENTAL and FROZEN (FEAT-1): unattended execution contradicts the read-only-first posture and is the only path that turns a prompt-injected finding into a cluster mutation, so it gets no further investment and may be removed — prefer "approve", which captures nearly all the value. Recommend dry_run if you evaluate it.

func BuildCatalog

func BuildCatalog(ctx context.Context, cfg *config.Config, forgeTok ForgeToken, metrics *telemetry.Metrics, log *slog.Logger) *catalog.Catalog

BuildCatalog returns the kb_search backing store, or nil when no catalog is configured. With a Git URL it starts a background syncer (running on every replica, so a failover standby is already warm) that re-indexes after each pull; otherwise it loads a static mounted directory once.

func BuildCurator

func BuildCurator(cfg *config.Config, token ForgeToken, cat *catalog.Catalog, metrics *telemetry.Metrics, log *slog.Logger) *curator.Curator

BuildCurator returns a Curator when the GitHub App token + KB repo are configured, else nil.

func BuildFailureDebouncer

func BuildFailureDebouncer(cfg *config.Config, gp providers.GitOpsProvider, metrics *telemetry.Metrics, log *slog.Logger) *investigate.Debouncer

BuildFailureDebouncer wires a Debouncer whose "still failing?" re-check reads the workload's CURRENT Ready condition via GitOpsInspector.ResourceStatus. When the engine offers no inspector, the predicate is nil and the debouncer just waits out the window before enqueuing (still filtering instantly-clearing flaps). A zero window makes Debounce enqueue immediately.

func BuildGitOps

func BuildGitOps(cfg *config.Config, dc dynamic.Interface, log *slog.Logger) providers.GitOpsProvider

BuildGitOps builds the GitOps provider for the configured engine (flux default). The differ clones the GitOps source repo over HTTPS; it authenticates private repos with the shared GitHub App installation token (the App needs contents:read on the source repo). A nil token source (no App configured) means public/local repos only.

func BuildInvestigator

func BuildInvestigator(ctx context.Context, cfg *config.Config, gp providers.GitOpsProvider, approvals *action.Approvals, auto *action.Auto, metrics *telemetry.Metrics, ledger *outcome.Ledger, log *slog.Logger) (investigate.Investigator, *catalog.Catalog, error)

BuildInvestigator returns the LLM ReAct investigator when a model is configured, otherwise the read-only LogInvestigator. It also returns the catalog (nil when no model is configured or no catalog is wired).

func BuildJudgeModel

func BuildJudgeModel(cfg *config.Config, provider, baseURL, model, apiKeyEnv string) providers.ModelProvider

BuildJudgeModel builds the (stronger) grader model from --judge-* flags, falling back to the configured investigation model when unset.

func BuildMatrixFeedback added in v0.8.0

func BuildMatrixFeedback(cfg *config.Config, ledger *outcome.Ledger, log *slog.Logger) *notify.MatrixFeedback

BuildMatrixFeedback assembles the opt-in Matrix reaction listener (notify.matrix.feedback_reactions): nil unless the option is on, the outcome ledger persists, and the access token is actually present — a listener that could record nowhere or authenticate as no one must not start. Validate has already required the notifier fields and the ledger path with the option on; the token presence is an env-var runtime fact, checked here like the notifier's own builder does.

func BuildModel

func BuildModel(cfg *config.Config, apiKey string) providers.ModelProvider

BuildModel builds the ModelProvider for the configured provider, applying the effective output-token cap (model.max_tokens, defaulted when unset) and the opt-in effort and thinking knobs.

func BuildModelAndTools

BuildModelAndTools assembles the model, investigation tools, and the instant-recall short-circuit from config + the GitOps provider. Shared by serve and investigate.

func BuildNotifier

func BuildNotifier(cfg *config.Config, log *slog.Logger) (*notify.Multi, error)

BuildNotifier assembles the configured chat notifiers (best-effort fan-out) via the notifier registry. Slack/Matrix (and any registered sink, e.g. the generic webhook) self-register; each Build reads its own config.

func BuildReinvestigator

func BuildReinvestigator(ctx context.Context, cfg *config.Config, gp providers.GitOpsProvider, metrics *telemetry.Metrics, log *slog.Logger) *investigate.Reinvestigator

BuildReinvestigator returns a poller that re-runs KB issues labelled "reinvestigate" and posts the fresh findings back, or nil when the forge isn't configured. RunLore polls the forge (outbound) — it has no inbound webhooks.

func BuildVerifyModel

func BuildVerifyModel(cfg *config.Config) providers.ModelProvider

BuildVerifyModel builds the optional cheaper model for the adversarial verify pass, inheriting any unset field from the main model. Returns nil when no model.verify override is configured (verify then runs on the main model).

func CatalogConfigured

func CatalogConfigured(cfg *config.Config) bool

CatalogConfigured reports whether the operator asked for a knowledge catalog (a mounted dir or a git-sync repo). It is independent of whether the load succeeded.

func CatalogExpected added in v0.7.0

func CatalogExpected(cfg *config.Config) bool

CatalogExpected reports whether readiness should gate on a built catalog: a catalog source AND a usable model. With a model, a configured-but-failed catalog (BuildCatalog returns nil) keeps the pod at /readyz 503 instead of collapsing readiness to pure leadership and serving incidents with no KB. Without a model, BuildInvestigator skips the catalog entirely (log-only investigator, cat=nil by design, not by failure), so gating on it would hold every replica — leader included — at 503 forever.

func EncodeLeaseIdentity added in v0.8.0

func EncodeLeaseIdentity(podName, podIP string) string

EncodeLeaseIdentity builds the routable lease identity. With no (or an invalid) pod IP it degrades to the bare name — the pre-#264 format — so a misconfigured POD_IP never publishes a dial target that cannot work.

func GitOpsFromKube

func GitOpsFromKube(cfg *config.Config, log *slog.Logger) providers.GitOpsProvider

GitOpsFromKube builds the GitOps provider from the ambient kubeconfig (best-effort).

func GitopsEngine

func GitopsEngine(cfg *config.Config) string

GitopsEngine returns the configured GitOps engine, defaulting to flux.

func KubeClientset

func KubeClientset(log *slog.Logger) *kubernetes.Clientset

KubeClientset builds a read-only clientset for pod-log access, or nil when no cluster is reachable (e.g. local runs without a kubeconfig).

func LogLedgerStartup

func LogLedgerStartup(log *slog.Logger, s outcome.Status)

LogLedgerStartup reports, at the right level, what the outcome ledger looks like to this `lore curate` process — turning the previously-silent no-op into a visible warning. The Queue + Recurrence passes read the ledger, but outcome.New succeeds even when the file is absent, so a misconfigured mount (the ledger lives on a volume the CronJob doesn't see — e.g. persistence not enabled, the path not under catalog.mountPath, or a fresh per-Job emptyDir) would silently produce zero work. We still run the passes; we just make the likely misconfiguration loud.

func ModelConfigured

func ModelConfigured(cfg *config.Config) bool

ModelConfigured reports whether a usable model is configured: a provider with a built-in default endpoint (anthropic, gemini), or any provider with a base_url.

func ModelProvider

func ModelProvider(cfg *config.Config) string

ModelProvider returns the configured model provider name (default "openai").

func NewHTTPServer

func NewHTTPServer(addr string, h http.Handler) *http.Server

NewHTTPServer builds the serving http.Server with every inbound bound set. Go's zero defaults leave each of these unlimited, exposing the long-lived server to Slowloris (slow header/body), unbounded idle keep-alives, and oversized headers. Payloads (Alertmanager/Slack) are small and synchronous, so 30s read/write is generous while still cutting off slow attackers; the body itself is capped per handler (1 MiB).

func NewModelClient

func NewModelClient(provider, baseURL, model, apiKey string, maxTokens int, effort, thinking string) providers.ModelProvider

NewModelClient builds a ModelProvider for a wire protocol + endpoint. maxTokens is the per-request output-token ceiling passed through to the provider. effort is the opt-in reasoning knob (config-validated per provider; "" = omitted); gemini does not take it — config.Validate rejects effort for that provider. thinking is the opt-in adaptive-thinking knob (anthropic-only, config-validated; "" = omitted); only the anthropic client consumes it.

func OutcomeKind

func OutcomeKind(recalled bool) string

OutcomeKind labels an outcome-ledger open as a recall (cache hit) or a fresh finding.

func ParseLeaseIdentity added in v0.8.0

func ParseLeaseIdentity(id string) (name, ip string)

ParseLeaseIdentity splits a lease holder identity into name and IP. It is deliberately defensive: an OLD-format identity (no "_", from a pre-#264 replica during a mixed-version rollout) or a suffix that is not a valid IP yields ip == "" — forwarding is then unavailable and callers shed with 503 + Retry-After instead of dialing garbage.

func PodIP added in v0.8.0

func PodIP() string

PodIP returns this pod's IP from the downward API (POD_IP, set by the chart). Empty outside Kubernetes or on a chart that predates #264 — the lease identity then degrades to the name-only format and leader forwarding is unavailable (followers answer 503 + Retry-After instead of proxying).

func PodName

func PodName() string

PodName returns this pod's identity for leader election.

func PodNamespace

func PodNamespace() string

PodNamespace resolves the namespace from the downward API or the service-account mount.

func ReadyFunc

func ReadyFunc(cat *catalog.Catalog, configured bool) func() bool

ReadyFunc gates readiness on process + catalog health — and deliberately NOT on leadership (#264). Readiness used to also require holding the leader Lease, which kept every standby replica permanently NotReady: with replicaCount>1, `helm upgrade --wait` / kstatus (Flux helm-controller) could never observe the release Ready and timed out on every upgrade. Every warm replica now reports Ready; "only the leader processes work" is preserved by the forwarding layer instead (server.Forward proxies work-bearing requests from followers to the leader learned from the Lease).

The catalog gate stays: a replica must NOT advertise ready until its knowledge base is loaded and warm — otherwise it would serve incident traffic blind (and, as a follower, could be promoted to leader cold). This distinguishes the two ways BuildCatalog returns a nil catalog:

  • configured but the load failed (configured=true, cat=nil): stay 503. A static catalog has no syncer to recover, so the pod stays not-ready and the misconfiguration surfaces loudly instead of silently serving with no KB.
  • not configured at all (configured=false, cat=nil): no catalog gate; the replica is ready as soon as it serves.

A configured-but-not-yet-warm catalog (git-sync NewEmpty, cat!=nil && !Ready()) is also held at 503 until its first successful sync.

func RequirePagerDutyAuth added in v0.3.0

func RequirePagerDutyAuth(cfg *config.Config, enabled bool, secret string) error

RequirePagerDutyAuth is the PagerDuty analogue of RequireWebhookAuth. The PagerDuty source authenticates /webhook/pagerduty with its own X-PagerDuty-Signature verification (not the shared server.webhook_token_env bearer token), so the shared guard does not cover it. When the source is enabled and a model is configured, it must carry a signing secret — otherwise an unauthenticated caller could drive (and bill) the model. Fail closed on the serve path only, mirroring RequireWebhookAuth.

func RequireWebhookAuth

func RequireWebhookAuth(cfg *config.Config, webhookToken string) error

RequireWebhookAuth fails closed on the serve path when the LLM investigator is wired but the alert webhook is anonymous. The webhook's labels/annotations flow verbatim into the LLM prompt (and bill the model), so an unauthenticated caller must not reach it once a model is configured — regardless of actions.mode. This lives on the serve path, NOT in config.Validate: Validate is shared by every subcommand (e.g. `lore investigate` legitimately needs a model and has no webhook), so the requirement is scoped to where the webhook is actually served. It mirrors the approval-token fail-closed guard.

func RestConfig

func RestConfig() (*rest.Config, error)

RestConfig builds a Kubernetes REST config from in-cluster config, falling back to the local kubeconfig (respecting $KUBECONFIG, then ~/.kube/config).

func RunCatalog

func RunCatalog(args []string) error

RunCatalog dispatches catalog subcommands.

func RunCatalogSync

func RunCatalogSync(args []string) error

RunCatalogSync clones/pulls the catalog Git repo into the mirror and reports the indexed entry count — a one-shot of the background sync that serve runs.

func RunCurate

func RunCurate(args []string) error

RunCurate grooms the KB backlog (Phase-2 curation agent). It runs the backlog-dedup pass (collapses duplicate open PRs across history) and the lifecycle sweep (closes stale, unprotected PRs by forge age). When outcome.ledger_path is configured, it also runs the Queue pass (promotes solved→ready-to-merge when the incident resolves), the Recurrence pass (opens a knowledge-gap issue for repeatedly-unresolved patterns), and the Contested pass (warns the open KB PR when humans 👎'd the investigation behind it).

func RunEval

func RunEval(args []string) error

RunEval replays recorded incident cases through the investigation loop and reports the RCA-identification rate. Requires a configured model.

func RunEvalCompare added in v0.3.0

func RunEvalCompare(cfg *config.Config, comparePath, casesDir, reportDir, stamp string, n int,
	jProvider, jBaseURL, jModel, jKeyEnv string) error

RunEvalCompare benchmarks every model in the comparison spec against the replay suite and writes one aggregated report (markdown + JSON) to reportDir. The judge is fixed across entries (blind grading already anonymizes which model produced a result), so scores are comparable. It reuses the single-run replay machinery per entry via eval.ComparisonRunner.

func RunEvalLive

func RunEvalLive(cfg *config.Config, scnDir, recordDir, reportDir, prevReport, stamp string, n int,
	jProvider, jBaseURL, jModel, jKeyEnv string) error

RunEvalLive runs the live-fire campaign and writes a dated report.

func RunInvestigate

func RunInvestigate(args []string) error

RunInvestigate runs a single on-demand investigation and prints the findings.

func RunKB added in v0.7.0

func RunKB(args []string) error

RunKB dispatches the human-facing knowledge-base read commands. `lore catalog sync` remains the machine/ops write surface; `lore kb` is how a person asks "what do we already know about this?" without an MCP client or a cluster.

func RunLeaderElection

func RunLeaderElection(ctx context.Context, cfg *config.Config, cs kubernetes.Interface, leader *atomic.Bool, tracker *LeaderTracker, log *slog.Logger, startWork func(context.Context))

RunLeaderElection blocks running Lease-based leader election; the leader runs startWork. Lost leadership cancels the work context and the replica REJOINS the election as a standby: client-go's RunOrDie returns when the lease is lost without the process dying (e.g. an API-server blip past RenewDeadline), and without the re-entry loop the replica would never contend again — a permanent zombie standby that /healthz keeps reporting alive, silently shrinking the HA pool until no replica leads at all.

The lease identity is "<podName>_<podIP>" (#264): every replica learns the holder through OnNewLeader and records it in tracker, so a follower can PROXY incoming work-bearing requests to the leader (server.Forward) — /readyz no longer reflects leadership, so the Service routes to any warm replica. tracker may be nil (no forwarding wired, e.g. tests).

func RunMCP

func RunMCP(version string, args []string) error

RunMCP serves RunLore capabilities over the Model Context Protocol (stdio JSON-RPC): the GitOps what-changed tool (needs a Kubernetes client + config.gitops) and the knowledge-base tools kb_search/kb_get (need an OKF catalog dir). Each capability is optional — an MCP client on a laptop can serve just the KB from a local checkout — but an empty server is an error. stdout is the protocol channel; logs go to stderr. Read-only. version is injected at build time and stays in package main; it is passed through to the MCP server's advertised server info.

Usage: lore mcp [--config runlore.yaml] [catalog-dir] The positional catalog-dir overrides config catalog.dir; when it is given, a missing config file is tolerated (KB-only mode needs no config).

func RunServe

func RunServe(version string, args []string) error

RunServe runs the in-cluster agent: it loads config, builds the investigation pipeline (kube/GitOps/model/catalog/actions), runs leader election, and serves the webhook + readiness endpoints until SIGTERM, then drains gracefully. version is injected at build time and stays in package main; it is passed through for the build-info gauge.

func ServePort added in v0.8.0

func ServePort(addr string) string

ServePort extracts the port from a listen address (":8080", "0.0.0.0:8080"). "" when the address does not parse — the forwarding layer then reports no routable leader instead of building a broken URL.

func SetMemoryLimitFromCgroup

func SetMemoryLimitFromCgroup(log *slog.Logger)

SetMemoryLimitFromCgroup sets GOMEMLIMIT to ~90% of the cgroup v2 memory limit so the Go GC respects the container's memory cap — keeping the heap under a soft ceiling and returning memory to the OS under pressure — instead of letting RSS grow across investigations until the cgroup OOM-kills the process. No-op when GOMEMLIMIT is set explicitly, or there is no cgroup memory limit.

func WebhookAuthWarning added in v0.8.0

func WebhookAuthWarning(alertmanagerEnabled bool, webhookToken string, mode config.ActionMode) string

WebhookAuthWarning decides the startup warning for an unauthenticated alert webhook, returning "" when no warning is warranted. Unlike RequireWebhookAuth (which only fails closed once a model is billed per request) and config.Validate (which only hard-fails actions.mode=auto), an empty token is a DELIBERATE fail-open default outside those cases — cluster-internal deployments legitimately skip it. That default must not be silent: whenever the alertmanager source is enabled with no token, anyone who can reach the Service can trigger investigations (and, under an executing rung, actions a human then approves) regardless of whether a model is configured yet. PagerDuty is deliberately excluded — it authenticates via its own X-PagerDuty-Signature scheme (see RequirePagerDutyAuth), so an empty shared token says nothing about its exposure. actions.mode=approve escalates the wording (a human is now clicking "run" off of webhook-supplied data) but stays a warning, not an error — auto already hard-fails via config.Validate, and off/suggest carry no execution risk to call out.

Types

type ForgeToken

type ForgeToken func(context.Context) (string, error)

ForgeToken mints GitHub App installation tokens.

func BuildForgeTokenSource

func BuildForgeTokenSource(cfg *config.Config, log *slog.Logger) ForgeToken

BuildForgeTokenSource builds the GitHub App installation-token source shared by the curator (issues/PRs) and catalog git-sync (clone auth) — one identity for both forge writes and reads. Returns nil when no App is configured.

type LeaderTracker added in v0.8.0

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

LeaderTracker atomically tracks the current lease holder identity as reported by the leader-election OnNewLeader callback, so a follower can route work-bearing requests to the leader. Safe for concurrent use: the election goroutine writes, every request-serving goroutine reads.

The view can be briefly stale (the holder just died and no successor has renewed yet) — the forwarding layer treats a failed dial as "retry shortly" (503 + Retry-After) rather than trusting the tracker blindly.

func (*LeaderTracker) Addr added in v0.8.0

func (t *LeaderTracker) Addr(port string) string

Addr returns "ip:port" (IPv6 bracketed) for the current holder on the given serve port — every replica serves on the same port, so the follower's own port is the leader's too. It returns "" when no holder is known, the holder's identity carries no routable IP (old format), or port is empty.

func (*LeaderTracker) Identity added in v0.8.0

func (t *LeaderTracker) Identity() string

Identity returns the last observed holder identity ("" before the first OnNewLeader callback).

func (*LeaderTracker) Name added in v0.8.0

func (t *LeaderTracker) Name() string

Name returns the pod-name part of the current holder identity ("" before the first OnNewLeader callback, or from a bare pre-#264 identity with no IP). The forwarding layer compares it against this replica's own name to detect the tracker pointing at itself during a takeover and serve locally rather than proxy to itself (server.Forward.LeaderName).

func (*LeaderTracker) Set added in v0.8.0

func (t *LeaderTracker) Set(identity string)

Set records the observed holder identity (called from OnNewLeader).

Jump to

Keyboard shortcuts

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