From 28dcd5b8e6b021954cfa2fa593a66469139b9689 Mon Sep 17 00:00:00 2001 From: Alexandru Macocian Date: Sat, 13 Jun 2026 11:43:26 +0200 Subject: [PATCH] Grafana MCP --- README.md | 1 + cmd/gitea-mcp/client.go | 10 +- cmd/gitea-mcp/main.go | 13 +- cmd/grafana-mcp/grafana_clientid_charlie.go | 21 + cmd/grafana-mcp/main.go | 274 ++++++++++++ cmd/gssh-mcp/client.go | 10 +- cmd/gssh-mcp/main.go | 14 +- cmd/gssh-mcp/tools_ssh.go | 14 +- cmd/sherlock/main.go | 15 +- docs/architecture.md | 2 +- docs/auth-model.md | 38 ++ docs/grafana-mcp.md | 136 ++++++ go.mod | 157 ++++++- go.sum | 466 +++++++++++++++++++- internal/authn/login_test.go | 31 ++ internal/authn/source.go | 234 ++++++++++ internal/authn/source_test.go | 173 ++++++++ plan.md | 2 + 18 files changed, 1575 insertions(+), 36 deletions(-) create mode 100644 cmd/grafana-mcp/grafana_clientid_charlie.go create mode 100644 cmd/grafana-mcp/main.go create mode 100644 docs/grafana-mcp.md create mode 100644 internal/authn/source.go create mode 100644 internal/authn/source_test.go diff --git a/README.md b/README.md index 07c16cb..0728ca3 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Design + phasing: [plan.md](plan.md). - [Storage & keyring](docs/storage.md) - [Agents](docs/agents.md) - [gitea-mcp](docs/gitea-mcp.md) +- [grafana-mcp](docs/grafana-mcp.md) - [gssh-mcp](docs/gssh-mcp.md) - [gssh integration](docs/gssh-integration.md) - [Conventions](docs/conventions.md) diff --git a/cmd/gitea-mcp/client.go b/cmd/gitea-mcp/client.go index fae7420..fd36dee 100644 --- a/cmd/gitea-mcp/client.go +++ b/cmd/gitea-mcp/client.go @@ -21,9 +21,13 @@ var ErrNotFound = errors.New("gitea: not found") // use. We don't pull in code.gitea.io/sdk/gitea because its client // only knows how to send `Authorization: token `, not the // `Authorization: Bearer ` that Gitea's OAuth2 server issues. +// +// token is a getter, not a static string: the sherlock TokenSource +// renews the short-lived access token in the background and pushes each +// new value in, so every request reads the currently-valid bearer. type giteaAPI struct { baseURL string - token string + token func() string client *http.Client } @@ -38,7 +42,7 @@ func (g *giteaAPI) get(ctx context.Context, path string, query url.Values, into if err != nil { return err } - req.Header.Set("Authorization", "Bearer "+g.token) + req.Header.Set("Authorization", "Bearer "+g.token()) req.Header.Set("Accept", "application/json") dbg("GET %s", full) resp, err := g.client.Do(req) @@ -70,7 +74,7 @@ func (g *giteaAPI) getRaw(ctx context.Context, path string, query url.Values, ma if err != nil { return nil, false, err } - req.Header.Set("Authorization", "Bearer "+g.token) + req.Header.Set("Authorization", "Bearer "+g.token()) req.Header.Set("Accept", "*/*") resp, err := g.client.Do(req) if err != nil { diff --git a/cmd/gitea-mcp/main.go b/cmd/gitea-mcp/main.go index 84267af..5c085fc 100644 --- a/cmd/gitea-mcp/main.go +++ b/cmd/gitea-mcp/main.go @@ -145,7 +145,8 @@ func main() { ClientSecret: giteaClientSecret, Scopes: scopes, } - ts, err := authn.Ensure(ctx, store, serviceName, cfg, authn.EnsureOptions{ + holder := authn.NewTokenHolder() + src := authn.NewTokenSource(store, serviceName, cfg, authn.EnsureOptions{ LockPath: lockPath, OnAuthURL: func(u string) { fmt.Fprintln(os.Stderr, "gitea-mcp: opening browser for Gitea OAuth login:") @@ -155,7 +156,8 @@ func main() { fmt.Fprintln(os.Stderr, "(visit the URL above manually)") } }, - }) + }, holder.Set) + ts, err := src.Start(ctx) if err != nil { fmt.Fprintln(os.Stderr, "gitea-mcp: auth:", err) os.Exit(5) @@ -164,9 +166,14 @@ func main() { dbg("startup: pid=%d access_token_prefix=%q scopes=%v id_expires_at=%s", os.Getpid(), tokenPrefix(ts.AccessToken), ts.Scopes, ts.IDExpiresAt.Format(time.RFC3339)) + // Keep the short-lived access token fresh for the life of the + // session. Renewals are pushed into holder via the callback above; + // the request path reads the current token through holder.AccessToken. + go func() { _ = src.Run(ctx) }() + api := &giteaAPI{ baseURL: strings.TrimRight(giteaBaseURL, "/"), - token: ts.AccessToken, + token: holder.AccessToken, client: &http.Client{Timeout: 30 * time.Second}, } diff --git a/cmd/grafana-mcp/grafana_clientid_charlie.go b/cmd/grafana-mcp/grafana_clientid_charlie.go new file mode 100644 index 0000000..c23c9a8 --- /dev/null +++ b/cmd/grafana-mcp/grafana_clientid_charlie.go @@ -0,0 +1,21 @@ +//go:build !noembed + +// This file bakes Charlie's homelab defaults for grafana-mcp into the +// binary. Enabled by default; build with `-tags noembed` to drop and +// supply your own values via -ldflags (see main.go). +// +// The default uses the existing Public Authentik `sherlock-cli` +// provider: the client ID is public, and no client_secret is involved. +package main + +func init() { + if grafanaIssuer == "" { + grafanaIssuer = "https://id.alexandru.macocian.me/application/o/sherlock-cli/" + } + if grafanaBaseURL == "" { + grafanaBaseURL = "https://grafana.alexandru.macocian.me" + } + if grafanaClientID == "" { + grafanaClientID = "1FN3B6EsTXwlN0qALiCoaKJLM23yuru80bHp5WfS" + } +} diff --git a/cmd/grafana-mcp/main.go b/cmd/grafana-mcp/main.go new file mode 100644 index 0000000..fbb3ef1 --- /dev/null +++ b/cmd/grafana-mcp/main.go @@ -0,0 +1,274 @@ +// Command grafana-mcp is sherlock's MCP server for Grafana. It embeds +// Grafana Labs' upstream mcp-grafana tool set (imported as a Go +// package, github.com/grafana/mcp-grafana) and authenticates as the +// operator via Authentik + PKCE, injecting the resulting bearer token +// into every Grafana API call. +// +// Unlike the upstream binary, grafana-mcp never reads a Grafana +// service-account token or API key. It uses sherlock's wallet +// (authn.Ensure) so Grafana access is the operator's own OAuth +// identity, refreshed transparently — the same model as gitea-mcp and +// gssh-mcp. Only read-only tool categories are registered. +// +// For this to work, the Grafana deployment must accept the +// Authentik-issued JWT on its HTTP API via Grafana's `[auth.jwt]` +// integration (Grafana otherwise treats `Authorization: Bearer ...` as +// a service-account token and returns "Invalid API key"). This is a +// distinct mechanism from `[auth.generic_oauth]`, which only logs +// browser users in and yields a session cookie. See docs/grafana-mcp.md. +// +// Two modes: +// +// grafana-mcp start the stdio MCP server (agents spawn this) +// grafana-mcp --probe run auth + GET /api/user, print result, exit +// +// Configuration is compile-time; Charlie defaults live in +// grafana_clientid_charlie.go and are baked in by default. Use +// `-tags noembed` to retarget via -ldflags. +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/signal" + "strings" + "syscall" + "time" + + mcpgrafana "github.com/grafana/mcp-grafana" + "github.com/grafana/mcp-grafana/tools" + "github.com/mark3labs/mcp-go/server" + + "gitea.alexandru.macocian.me/Charlie/sherlock/internal/agent" + "gitea.alexandru.macocian.me/Charlie/sherlock/internal/authn" + "gitea.alexandru.macocian.me/Charlie/sherlock/internal/browser" + "gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring" + "gitea.alexandru.macocian.me/Charlie/sherlock/internal/xdg" +) + +// serviceName is the wallet key under which grafana tokens live. The +// sherlock CLI's `logout grafana` clears the same entry. +const serviceName = "grafana" + +// Compile-time configuration. Defaults target the Charlie homelab +// (see grafana_clientid_charlie.go). To retarget: +// +// go build -tags noembed \ +// -ldflags "-X main.grafanaIssuer=https://id.example/.../ \ +// -X main.grafanaClientID=... \ +// -X main.grafanaBaseURL=https://grafana.example" \ +// ./cmd/grafana-mcp +var ( + grafanaIssuer string + grafanaClientID string + grafanaClientSecret string + grafanaBaseURL string +) + +// Version is overwritten at build time via -ldflags "-X main.Version=...". +var Version = "0.0.0-dev" + +// scopes asked from Authentik. `openid` is required for an ID token; +// `profile`/`email` populate the claims Grafana's JWT integration maps +// onto the operator's Grafana account. +var scopes = []string{"openid", "profile", "email"} + +func main() { + probe := flag.Bool("probe", false, "run auth + Grafana /api/user, print result, exit") + showVersion := flag.Bool("version", false, "print version and exit") + flag.Parse() + if *showVersion { + fmt.Println(Version) + return + } + + if grafanaClientID == "" { + fmt.Fprintln(os.Stderr, "grafana-mcp: grafanaClientID not configured.") + fmt.Fprintln(os.Stderr, "Build with -ldflags \"-X main.grafanaClientID=...\" (and -tags noembed if you don't want the Charlie default) after creating/configuring an Authentik OAuth2 application (redirect URI http://127.0.0.1:6990/callback).") + os.Exit(2) + } + if grafanaBaseURL == "" { + fmt.Fprintln(os.Stderr, "grafana-mcp: grafanaBaseURL not configured.") + os.Exit(2) + } + + store, err := keyring.Open() + if err != nil { + fmt.Fprintln(os.Stderr, "grafana-mcp:", err) + os.Exit(3) + } + + agent.EnsurePathContainsSiblings() + + lockPath, err := xdg.RefreshLockPath() + if err != nil { + fmt.Fprintln(os.Stderr, "grafana-mcp:", err) + os.Exit(1) + } + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + cfg := authn.Config{ + Issuer: grafanaIssuer, + ClientID: grafanaClientID, + ClientSecret: grafanaClientSecret, + Scopes: scopes, + } + holder := authn.NewTokenHolder() + src := authn.NewTokenSource(store, serviceName, cfg, authn.EnsureOptions{ + LockPath: lockPath, + OnAuthURL: func(u string) { + fmt.Fprintln(os.Stderr, "grafana-mcp: opening browser for Grafana OAuth login:") + fmt.Fprintln(os.Stderr, " ", u) + if err := browser.Open(u); err != nil { + fmt.Fprintln(os.Stderr, "grafana-mcp: open browser:", err) + fmt.Fprintln(os.Stderr, "(visit the URL above manually)") + } + }, + }, holder.Set) + ts, err := src.Start(ctx) + if err != nil { + fmt.Fprintln(os.Stderr, "grafana-mcp: auth:", err) + os.Exit(5) + } + if ts.AccessToken == "" { + fmt.Fprintln(os.Stderr, "grafana-mcp: OAuth session did not include an access token") + os.Exit(5) + } + + base := strings.TrimRight(grafanaBaseURL, "/") + + if *probe { + if err := probeGrafana(ctx, base, ts.AccessToken); err != nil { + fmt.Fprintln(os.Stderr, "grafana-mcp: probe:", err) + os.Exit(5) + } + return + } + + // Keep the short-lived access token fresh for the whole session. + // Run pushes each renewed token into holder via the callback above, + // so the per-request transport always sends a valid bearer. + go func() { _ = src.Run(ctx) }() + + if err := serve(ctx, base, holder); err != nil && !errors.Is(err, context.Canceled) { + fmt.Fprintln(os.Stderr, "grafana-mcp:", err) + os.Exit(1) + } +} + +// bearerTransport injects the current sherlock-managed bearer token +// into every Grafana API request. It reads the token from the holder +// on each call, so token renewals (pushed by the TokenSource) take +// effect immediately without rebuilding the Grafana client. +type bearerTransport struct { + base http.RoundTripper + holder *authn.TokenHolder +} + +func (t *bearerTransport) RoundTrip(req *http.Request) (*http.Response, error) { + bearer, err := t.holder.Bearer() + if err != nil { + return nil, err + } + r := req.Clone(req.Context()) + r.Header.Set("Authorization", bearer) + return t.base.RoundTrip(r) +} + +// serve registers the read-only upstream tool set and runs the stdio +// MCP server. The operator's OAuth bearer token is injected into every +// Grafana API request via a custom base transport (GrafanaConfig. +// BaseTransport), which reads the current token from holder per call. +func serve(ctx context.Context, baseURL string, holder *authn.TokenHolder) error { + s := server.NewMCPServer("grafana-mcp", Version, + server.WithInstructions("Read-only access to the operator's Grafana instance, authenticated via sherlock-managed Authentik OAuth."), + ) + + // Read-only categories only. The `false` argument disables every + // write tool (create/update/delete) in the categories that have + // them, mirroring upstream `--disable-write`. + tools.AddSearchTools(s) + tools.AddDatasourceTools(s) + tools.AddPrometheusTools(s) + tools.AddLokiTools(s) + tools.AddAlertingTools(s, false) + tools.AddDashboardTools(s, false) + tools.AddFolderTools(s, false) + tools.AddNavigationTools(s, false) + tools.AddAnnotationTools(s, false) + + // APIKey is intentionally empty: auth is handled by BaseTransport, + // which sends a freshly-renewed bearer on every request. + gc := mcpgrafana.GrafanaConfig{ + URL: baseURL, + BaseTransport: &bearerTransport{base: http.DefaultTransport, holder: holder}, + } + cf := mcpgrafana.ComposeStdioContextFuncs( + func(ctx context.Context) context.Context { + return mcpgrafana.WithGrafanaConfig(ctx, gc) + }, + func(ctx context.Context) context.Context { + return mcpgrafana.WithGrafanaClient(ctx, mcpgrafana.NewGrafanaClient(ctx, baseURL, "", nil)) + }, + ) + + srv := server.NewStdioServer(s) + srv.SetContextFunc(cf) + return srv.Listen(ctx, os.Stdin, os.Stdout) +} + +// probeGrafana verifies the OAuth bearer token is accepted by Grafana's +// HTTP API. A 401/403 here means OAuth succeeded but Grafana (or the +// proxy in front of it) is not validating the Authentik JWT for API +// access — see docs/grafana-mcp.md for the required `[auth.jwt]` setup. +func probeGrafana(ctx context.Context, baseURL, token string) error { + u, err := url.JoinPath(baseURL, "/api/user") + if err != nil { + return fmt.Errorf("build /api/user URL: %w", err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+token) + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + suffix := strings.TrimSpace(string(body)) + if suffix != "" { + suffix = ": " + suffix + } + return fmt.Errorf("GET /api/user returned HTTP %d%s; Grafana is not accepting the sherlock OAuth bearer token (enable Grafana [auth.jwt] — see docs/grafana-mcp.md)", resp.StatusCode, suffix) + } + + var user struct { + Login string `json:"login"` + Email string `json:"email"` + Name string `json:"name"` + } + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + return fmt.Errorf("decode /api/user: %w", err) + } + display := user.Login + if display == "" { + display = user.Name + } + fmt.Printf("OK: logged in to %s as %s <%s>\n", baseURL, display, user.Email) + return nil +} diff --git a/cmd/gssh-mcp/client.go b/cmd/gssh-mcp/client.go index a31a325..93eeb09 100644 --- a/cmd/gssh-mcp/client.go +++ b/cmd/gssh-mcp/client.go @@ -20,9 +20,13 @@ var ErrUnauthorized = errors.New("gssh: 401 unauthorized") // gsshAPI is a thin wrapper around the small REST surface Gssh // exposes alongside its WebSocket routes. The exec WebSocket itself // goes through internal/wsdatagram (see tools_ssh.go). +// +// token is a getter, not a static string: the sherlock TokenSource +// renews the short-lived access token in the background and pushes each +// new value in, so every REST call and WS dial reads the current bearer. type gsshAPI struct { baseURL string - token string + token func() string client *http.Client } @@ -35,7 +39,7 @@ func (g *gsshAPI) get(ctx context.Context, path string, query url.Values, into a if err != nil { return err } - req.Header.Set("Authorization", "Bearer "+g.token) + req.Header.Set("Authorization", "Bearer "+g.token()) req.Header.Set("Accept", "application/json") dbg("GET %s", full) resp, err := g.client.Do(req) @@ -61,7 +65,7 @@ func (g *gsshAPI) post(ctx context.Context, path string, into any) error { if err != nil { return err } - req.Header.Set("Authorization", "Bearer "+g.token) + req.Header.Set("Authorization", "Bearer "+g.token()) req.Header.Set("Accept", "application/json") dbg("POST %s", full) resp, err := g.client.Do(req) diff --git a/cmd/gssh-mcp/main.go b/cmd/gssh-mcp/main.go index fe75978..a1ffa68 100644 --- a/cmd/gssh-mcp/main.go +++ b/cmd/gssh-mcp/main.go @@ -119,7 +119,8 @@ func main() { ClientSecret: gsshClientSecret, Scopes: scopes, } - ts, err := authn.Ensure(ctx, store, serviceName, cfg, authn.EnsureOptions{ + holder := authn.NewTokenHolder() + src := authn.NewTokenSource(store, serviceName, cfg, authn.EnsureOptions{ LockPath: lockPath, OnAuthURL: func(u string) { fmt.Fprintln(os.Stderr, "gssh-mcp: opening browser for Gssh OAuth login:") @@ -129,7 +130,8 @@ func main() { fmt.Fprintln(os.Stderr, "(visit the URL above manually)") } }, - }) + }, holder.Set) + ts, err := src.Start(ctx) if err != nil { fmt.Fprintln(os.Stderr, "gssh-mcp: auth:", err) os.Exit(5) @@ -138,9 +140,15 @@ func main() { dbg("startup: pid=%d access_token_prefix=%q scopes=%v id_expires_at=%s", os.Getpid(), tokenPrefix(ts.AccessToken), ts.Scopes, ts.IDExpiresAt.Format(time.RFC3339)) + // Keep the short-lived access token fresh for the life of the + // session. Renewals are pushed into holder via the callback above; + // both the REST and WebSocket paths read the current token through + // holder.AccessToken. + go func() { _ = src.Run(ctx) }() + api := &gsshAPI{ baseURL: strings.TrimRight(gsshBaseURL, "/"), - token: ts.AccessToken, + token: holder.AccessToken, client: &http.Client{Timeout: 30 * time.Second}, } diff --git a/cmd/gssh-mcp/tools_ssh.go b/cmd/gssh-mcp/tools_ssh.go index 6bc7a42..2854044 100644 --- a/cmd/gssh-mcp/tools_ssh.go +++ b/cmd/gssh-mcp/tools_ssh.go @@ -44,12 +44,12 @@ type runCommandInput struct { } type runCommandOutput struct { - Stdout string `json:"stdout"` - Stderr string `json:"stderr"` - ExitCode *int `json:"exit_code,omitempty"` - TimedOut bool `json:"timed_out"` - StdoutTruncated bool `json:"stdout_truncated,omitempty"` - StderrTruncated bool `json:"stderr_truncated,omitempty"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + ExitCode *int `json:"exit_code,omitempty"` + TimedOut bool `json:"timed_out"` + StdoutTruncated bool `json:"stdout_truncated,omitempty"` + StderrTruncated bool `json:"stderr_truncated,omitempty"` } func runCommandHandler(api *gsshAPI) mcp.ToolHandlerFor[runCommandInput, runCommandOutput] { @@ -80,7 +80,7 @@ func execOverWebSocket(parent context.Context, api *gsshAPI, host, command strin dbg("WS dial %s", wsURL) headers := http.Header{} - headers.Set("Authorization", "Bearer "+api.token) + headers.Set("Authorization", "Bearer "+api.token()) conn, resp, err := websocket.Dial(ctx, wsURL, &websocket.DialOptions{ HTTPHeader: headers, diff --git a/cmd/sherlock/main.go b/cmd/sherlock/main.go index 4bdf0a0..5ae5ebb 100644 --- a/cmd/sherlock/main.go +++ b/cmd/sherlock/main.go @@ -199,15 +199,16 @@ func runAgent(name string, userArgs []string) { // to know about. Commands are resolved to absolute paths so the agent // CLI can spawn them regardless of how its own PATH is set up // (Copilot's Node-based subprocess spawner ignores parent env tweaks). -// Hardcoded for now — when the second MCP lands we'll extract this to -// an internal/mcp/registry/ package that mirrors the agent registry -// pattern. +// Hardcoded for now — when this grows more knobs we'll extract it to an +// internal/mcp/registry/ package that mirrors the agent registry pattern. func knownMCPs() (mcp.Servers, error) { out := mcp.Servers{} - for name, spec := range map[string]mcp.Server{ - "gitea": {Command: "gitea-mcp"}, - "gssh": {Command: "gssh-mcp"}, - } { + specs := map[string]mcp.Server{ + "gitea": {Command: "gitea-mcp"}, + "grafana": {Command: "grafana-mcp"}, + "gssh": {Command: "gssh-mcp"}, + } + for name, spec := range specs { abs, err := agent.LookPath(spec.Command) if err != nil { fmt.Fprintf(os.Stderr, "sherlock: skipping mcp %q: %v\n", name, err) diff --git a/docs/architecture.md b/docs/architecture.md index ae86cdc..07e62ed 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -45,7 +45,7 @@ There is no master session. | `internal/xdg/` | — | Resolves `$XDG_RUNTIME_DIR/sherlock/.mcp.json` and the refresh-lock path. | | `cmd/gitea-mcp/` (Phase 2) | — | First per-service MCP. Reads service config from `~/.config/sherlock/services.d/gitea.toml`, calls `authn.Ensure(store, "gitea", cfg, opts)` at startup, then serves MCP requests. | | `cmd/gssh-mcp/` (Phase 3) | — | Thin HTTP+WS client to the existing gssh server. No local certs. See [gssh-integration.md](gssh-integration.md). | -| `cmd/grafana-mcp/` (Phase 4) | — | Grafana tools, OAuth-federated via Authentik. | +| `cmd/grafana-mcp/` | — | Imports upstream `mcp-grafana` as a Go package and serves its read-only tools in-process; injects a sherlock-renewed OAuth bearer per request. Requires Grafana `[auth.jwt]`. See [grafana-mcp.md](grafana-mcp.md). | | `cmd/sherlock-mcp/` (Phase 4) | — | The only **interactive** MCP. Exposes `auth.list_sessions()`, `auth.revoke(service)`, etc., so the agent can query/manage the wallet through tool calls. | ## Why there is no daemon, and no `sherlock login` diff --git a/docs/auth-model.md b/docs/auth-model.md index 08cf0e2..feeac04 100644 --- a/docs/auth-model.md +++ b/docs/auth-model.md @@ -41,6 +41,44 @@ environment, the agent spawns its MCPs; each MCP triggers its own browser flow as it needs one. Subsequent runs are silent until the refresh window opens. +## Token renewal + +Authentik access tokens are short-lived (the `sherlock-cli` provider +issues ~5-minute tokens). An MCP that captured a token once at startup +would start getting 401s a few minutes into a long agent session. So +MCPs do not hold a static token — they hold a renewer. + +`authn.TokenSource` owns the refresh loop; the MCP supplies a +**mandatory callback** that the source invokes with every new token: + +```go +holder := authn.NewTokenHolder() +src := authn.NewTokenSource(store, "grafana", cfg, opts, holder.Set) + +ts, err := src.Start(ctx) // initial OAuth (browser on first use); calls holder.Set +go src.Run(ctx) // renews ahead of expiry; calls holder.Set on every rotation +``` + +- `Start(ctx)` does the initial `Ensure` and pushes the first token to + the callback. +- `Run(ctx)` sleeps until `RefreshSkew` before the token expires, + refreshes under the same cross-process flock as `EnsureFresh`, and — + when the access token actually rotates — invokes the callback again. + It is refresh-only and never opens a browser; if the refresh token + itself has expired it backs off and leaves re-login to the next + `sherlock` invocation. +- The callback is required (a nil callback panics at construction). The + request path never polls: it reads the current bearer from the + `TokenHolder` the callback writes (`holder.AccessToken` / + `holder.Bearer`), which is lock-free and always reflects the latest + renewal. + +This is why every MCP's API client takes a token *getter*, not a token +string: `grafana-mcp` reads it from a per-request `BaseTransport`, +`gitea-mcp` and `gssh-mcp` from `holder.AccessToken` on each REST call +and WebSocket dial. + + ## What lives in `services.d/.toml` (Phase 2) Each MCP needs an OIDC `Config` (issuer + client ID + scopes) to pass diff --git a/docs/grafana-mcp.md b/docs/grafana-mcp.md new file mode 100644 index 0000000..11616a7 --- /dev/null +++ b/docs/grafana-mcp.md @@ -0,0 +1,136 @@ +# grafana-mcp + +Sherlock's Grafana MCP **imports Grafana Labs' upstream +[`mcp-grafana`](https://github.com/grafana/mcp-grafana) as a Go +package** and serves a read-only subset of its tools in-process. There +is no separate `mcp-grafana` binary, no `uvx`, and no `exec`. + +It authenticates as the operator the same way `gitea-mcp` and +`gssh-mcp` do — OAuth + PKCE against Authentik, token stored in the OS +keyring — so the agent never sees a Grafana service-account token. + +## How auth works + +`grafana-mcp` does **not** use `GRAFANA_SERVICE_ACCOUNT_TOKEN`, +`GRAFANA_API_KEY`, or Grafana username/password auth. + +At startup it calls `authn.NewTokenSource(...).Start(ctx)` to obtain an +Authentik access token (browser flow on first use). Every Grafana API +request then carries that token as `Authorization: Bearer `, +injected by a custom `GrafanaConfig.BaseTransport`. The token is kept +fresh by the sherlock `TokenSource` renewer (see +[auth-model.md](auth-model.md#token-renewal)) — Authentik access tokens +live only ~5 minutes, so a captured-once token would 401 mid-session. + +Only read-only tool categories are registered (search, datasource, +prometheus, loki, alerting, dashboard, folder, navigation, annotations); +upstream write tools are disabled. + +## Why generic OAuth is not enough (and what is) + +Grafana already federates with Authentik via +`GF_AUTH_GENERIC_OAUTH_*`. That is **not** the same mechanism and does +**not** make this MCP work: + +| | `GF_AUTH_GENERIC_OAUTH_*` | `grafana-mcp` → API | +|---|---|---| +| OAuth client | **Grafana itself** | **sherlock** (`sherlock-cli` provider) | +| Purpose | interactive browser login | programmatic API call | +| Credential | a Grafana **session cookie** | an Authentik **JWT bearer** | +| Grafana's job | requests the token, drops it for a cookie | **validate a token it never issued** | + +Generic OAuth logs humans into the UI; it never accepts an externally +minted bearer on `/api/*`. Presenting our bearer there makes Grafana +treat it as a service-account token and return `Invalid API key`. + +The mechanism that makes Grafana **accept** the Authentik JWT on its +API is a separate integration: **`[auth.jwt]`**. It must be enabled on +the Grafana deployment (it is missing from the `Charlie/victoriametrics` +stack today). `auth.jwt` and `generic_oauth` coexist fine — UI users +keep using SSO; sherlock uses JWT on the API. + +## Required Grafana server config + +Add to the `grafana` service environment in +`Charlie/victoriametrics/docker-compose.yml`: + +```yaml + GF_AUTH_JWT_ENABLED: "true" + GF_AUTH_JWT_HEADER_NAME: "Authorization" # Grafana strips the "Bearer " prefix + GF_AUTH_JWT_USERNAME_CLAIM: "preferred_username" # matches the generic_oauth login attr → same user + GF_AUTH_JWT_EMAIL_CLAIM: "email" + GF_AUTH_JWT_JWK_SET_URL: "https://id.alexandru.macocian.me/application/o/sherlock-cli/jwks/" + GF_AUTH_JWT_AUTO_SIGN_UP: "true" + GF_AUTH_JWT_ROLE_ATTRIBUTE_PATH: "contains(groups, 'admins') && 'Admin' || 'Viewer'" + GF_AUTH_JWT_ROLE_ATTRIBUTE_STRICT: "true" + # All Authentik providers share one signing key, so pin the issuer + # to sherlock-cli — otherwise any Authentik JWT would authenticate. + GF_AUTH_JWT_EXPECT_CLAIMS: '{"iss":"https://id.alexandru.macocian.me/application/o/sherlock-cli/"}' +``` + +The claim names above are verified against an actual `sherlock-cli` +access token, which is a full RS256 JWT carrying: `iss`, `aud`, +`preferred_username` (= the operator's username, same value as `sub`), +`email`, `name`, and `groups` (including `admins`). Using +`preferred_username` aligns with `GF_AUTH_GENERIC_OAUTH_LOGIN_ATTRIBUTE_PATH`, +so JWT auth maps to the **same** Grafana account as browser SSO. + +Grafana must be able to reach `GF_AUTH_JWT_JWK_SET_URL` from inside its +container. + +## Charlie defaults + +| Variable / `-X main.…` | Charlie default | +|------------------------|-----------------| +| `grafanaIssuer` | `https://id.alexandru.macocian.me/application/o/sherlock-cli/` | +| `grafanaBaseURL` | `https://grafana.alexandru.macocian.me` | +| `grafanaClientID` | same Public `sherlock-cli` client as `gssh-mcp` | +| `grafanaClientSecret` | `""` (Public, PKCE-only) | +| `Version` | `0.0.0-dev` | + +## Build & install + +```bash +cd ~/Dev/charlie/sherlock +go install ./cmd/grafana-mcp +``` + +That's it — the upstream tool set is compiled in. For other +deployments: + +```bash +go build -tags noembed \ + -ldflags "-X main.grafanaIssuer=https://id.example/application/o/sherlock-cli/ \ + -X main.grafanaClientID= \ + -X main.grafanaBaseURL=https://grafana.example" \ + ./cmd/grafana-mcp +``` + +## Verify without an agent + +```bash +grafana-mcp --probe +``` + +Expected once `[auth.jwt]` is configured: + +```text +OK: logged in to https://grafana.alexandru.macocian.me as <> +``` + +A 401/403 (e.g. `Invalid API key`) means OAuth itself succeeded but +Grafana is not yet validating the Authentik JWT for API access — apply +the server config above. Force a re-login with `sherlock logout grafana`. + +## Use with an agent + +`sherlock copilot` / `sherlock claude` automatically render an MCP +config listing `grafana-mcp` when the binary is installed. All exposed +tools are read-only (subject to Grafana RBAC): dashboard +search/inspection, datasource listing/querying (Prometheus, Loki, …), +alert-rule and notification reads, annotations, and navigation +deeplinks. + +This relies on Grafana-side `[auth.jwt]` being configured (above). On a +deployment where that isn't in place, the MCP still starts but every +tool call 401s; verify with `grafana-mcp --probe` first. diff --git a/go.mod b/go.mod index c2209fa..0a13683 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,173 @@ module gitea.alexandru.macocian.me/Charlie/sherlock -go 1.26 +go 1.26.3 require ( + github.com/coder/websocket v1.8.14 github.com/coreos/go-oidc/v3 v3.18.0 github.com/go-jose/go-jose/v4 v4.1.4 + github.com/grafana/mcp-grafana v0.15.2 + github.com/mark3labs/mcp-go v0.46.0 github.com/modelcontextprotocol/go-sdk v1.6.1 github.com/zalando/go-keyring v0.2.8 golang.org/x/oauth2 v0.36.0 ) require ( - github.com/coder/websocket v1.8.14 // indirect + connectrpc.com/connect v1.19.1 // indirect + github.com/PaesslerAG/gval v1.2.4 // indirect + github.com/PaesslerAG/jsonpath v0.1.1 // indirect + github.com/apache/arrow-go/v18 v18.5.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.4 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect + github.com/aws/smithy-go v1.24.2 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cheekybits/genny v1.0.0 // indirect + github.com/clipperhouse/displaywidth v0.6.2 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/danieljoos/wincred v1.2.3 // indirect + github.com/dennwc/varint v1.0.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.24.3 // indirect + github.com/go-openapi/errors v0.22.7 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/loads v0.23.3 // indirect + github.com/go-openapi/runtime v0.29.3 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/strfmt v0.26.1 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/fileutils v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/mangling v0.25.5 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + github.com/go-openapi/validate v0.25.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/gogo/googleapis v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/flatbuffers v25.12.19+incompatible // indirect + github.com/google/gnostic v0.7.1 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect github.com/google/jsonschema-go v0.4.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/grafana/amixr-api-go-client v0.0.28 // indirect + github.com/grafana/grafana-openapi-client-go v0.0.0-20260330113218-ee77c4f6f90e // indirect + github.com/grafana/grafana-plugin-sdk-go v0.290.1 // indirect + github.com/grafana/incident-go v0.0.0-20251003115753-d71681611ddd // indirect + github.com/grafana/otel-profiling-go v0.5.1 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect + github.com/grafana/pyroscope/api v1.3.2 // indirect + github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.7.0 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect + github.com/itchyny/gojq v0.12.19 // indirect + github.com/itchyny/timefmt-go v0.1.8 // indirect + github.com/jaegertracing/jaeger-idl v0.6.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/mattetti/filebuffer v1.0.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/oklog/run v1.2.0 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect + github.com/olekukonko/tablewriter v1.1.3 // indirect + github.com/pierrec/lz4/v4 v4.1.23 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 // indirect + github.com/prometheus/alertmanager v0.31.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/prometheus v0.311.3 // indirect + github.com/prometheus/sigv4 v0.4.1 // indirect github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.5.4 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/sys v0.43.0 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.34.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/tools v0.44.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ae642e7..ad644af 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,496 @@ +cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= +connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= +github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU= +github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= +github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= +github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= +github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/apache/arrow-go/v18 v18.5.1 h1:yaQ6zxMGgf9YCYw4/oaeOU3AULySDlAYDOcnr4LdHdI= +github.com/apache/arrow-go/v18 v18.5.1/go.mod h1:OCCJsmdq8AsRm8FkBSSmYTwL/s4zHW9CqxeBxEytkNE= +github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= +github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= +github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A= github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= +github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= +github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= +github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= +github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c= +github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= +github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= +github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= +github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/gnostic v0.7.1 h1:t5Kc7j/8kYr8t2u11rykRrPPovlEMG4+xdc/SpekATs= +github.com/google/gnostic v0.7.1/go.mod h1:KSw6sxnxEBFM8jLPfJd46xZP+yQcfE8XkiqfZx5zR28= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= +github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grafana/amixr-api-go-client v0.0.28 h1:wh4aeEFVVJPv68YpxJAoP03BTUkLk6yLVSqPpF6yPYc= +github.com/grafana/amixr-api-go-client v0.0.28/go.mod h1:hU0Hq74HITX7RfcP0uZ7+F0/L+TL3N6JSzofBm2vzco= +github.com/grafana/grafana-openapi-client-go v0.0.0-20260330113218-ee77c4f6f90e h1:P/SerquhsX/4a3kreHR5mrJmohlWRqaxei/ZUSA6zkA= +github.com/grafana/grafana-openapi-client-go v0.0.0-20260330113218-ee77c4f6f90e/go.mod h1:sMcpxegie6TcvI6eVm+MbNneNC249GGWRcEO1M+UfSE= +github.com/grafana/grafana-plugin-sdk-go v0.290.1 h1:wNX4R8sHxEAmtmFhmV05IsLGznzdMKvN94v+2Pz8Wkw= +github.com/grafana/grafana-plugin-sdk-go v0.290.1/go.mod h1:KDkcxp1XqbKz0WD/q9p98Cf5Wp50LG0NkoPlVlptSWs= +github.com/grafana/incident-go v0.0.0-20251003115753-d71681611ddd h1:y8uJA/UmFHjwNWAvppxDRq+w9zIQmb0z79YVV2vS96g= +github.com/grafana/incident-go v0.0.0-20251003115753-d71681611ddd/go.mod h1:3QDfdZOWKRxNhMJFL+0C/+12+jLNHDlt0VKNr/i9Daw= +github.com/grafana/mcp-grafana v0.15.2 h1:E2OqH8mM4M9SiyhByxjT9toFnPG0IVWxthzKUPozl34= +github.com/grafana/mcp-grafana v0.15.2/go.mod h1:LT3LLGh4ZmjqasENuFicV8iJkyuGuVNZztyPolDlBqI= +github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= +github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grafana/pyroscope/api v1.3.2 h1:+F5JNUlM4ifoweKmRXW9cA6JsCeHzDVFV2Om3smqCxI= +github.com/grafana/pyroscope/api v1.3.2/go.mod h1:IQdc2koLAWVLlWcvBV4bm6uSFW2LiklKa8xyevsZ28I= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/itchyny/gojq v0.12.19 h1:ttXA0XCLEMoaLOz5lSeFOZ6u6Q3QxmG46vfgI4O0DEs= +github.com/itchyny/gojq v0.12.19/go.mod h1:5galtVPDywX8SPSOrqjGxkBeDhSxEW1gSxoy7tn1iZY= +github.com/itchyny/timefmt-go v0.1.8 h1:1YEo1JvfXeAHKdjelbYr/uCuhkybaHCeTkH8Bo791OI= +github.com/itchyny/timefmt-go v0.1.8/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= +github.com/jaegertracing/jaeger-idl v0.6.0 h1:LOVQfVby9ywdMPI9n3hMwKbyLVV3BL1XH2QqsP5KTMk= +github.com/jaegertracing/jaeger-idl v0.6.0/go.mod h1:mpW0lZfG907/+o5w5OlnNnig7nHJGT3SfKmRqC42HGQ= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 h1:SwcnSwBR7X/5EHJQlXBockkJVIMRVt5yKaesBPMtyZQ= +github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6/go.mod h1:WrYiIuiXUMIvTDAQw97C+9l0CnBmCcvosPjN3XDqS/o= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.46.0 h1:8KRibF4wcKejbLsHxCA/QBVUr5fQ9nwz/n8lGqmaALo= +github.com/mark3labs/mcp-go v0.46.0/go.mod h1:JKTC7R2LLVagkEWK7Kwu7DbmA6iIvnNAod6yrHiQMag= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= +github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU= github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM= +github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= +github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= +github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU= +github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 h1:S1hI5JiKP7883xBzZAr1ydcxrKNSVNm7+3+JwjxZEsg= +github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25/go.mod h1:ZQntvDG8TkPgljxtA0R9frDoND4QORU1VXz015N5Ks4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/alertmanager v0.31.1 h1:eAmIC42lzbWslHkMt693T36qdxfyZULswiHr681YS3Q= +github.com/prometheus/alertmanager v0.31.1/go.mod h1:zWPQwhbLt2ybee8rL921UONeQ59Oncash+m/hGP17tU= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856 h1:1Y6bmpZb8peQCy1IpctnAhIFuyhrdtMaDnETChhSNns= +github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856/go.mod h1:Vf0QcmVhGqpjLxZOaWrFSep86vchQtJmbztFaMM4f6Q= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/prometheus v0.311.3 h1:3IrVxQv6v5i/ZCGi6OrYeBhtCwaPTn6Z3DYruXoYm3M= +github.com/prometheus/prometheus v0.311.3/go.mod h1:gjsCxTKtHO1Q8T9333u1s+lUR1OjPyM7ruuGH8RvVyo= +github.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuXs= +github.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0 h1:c9r/G1CSw4dPI1jaNNG9RnQP+q4SvZnHciDQJVIvchU= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0/go.mod h1:gO9smoZe9KnZcJCqcB0lMmQ4Z5VEifYmjMTpnwtTSuQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 h1:aXl9uobjJs5vquMLt9ZkI/3zIuz8XQ3TqOKSWx0/xdU= +go.opentelemetry.io/contrib/propagators/jaeger v1.40.0/go.mod h1:ioMePqe6k6c/ovXSkmkMr1mbN5qRBGJxNTVop7/2XO0= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.34.0 h1:RZjNfF9OoR4oPLEWaP+Memql2MNVkZvnwjB2N5tR3cA= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.34.0/go.mod h1:b5U9IcSnv+lMvEcSOXZB61kXSf0KkwickleKWuAQclw= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= +google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= diff --git a/internal/authn/login_test.go b/internal/authn/login_test.go index 79fe918..c37317a 100644 --- a/internal/authn/login_test.go +++ b/internal/authn/login_test.go @@ -91,6 +91,37 @@ func newStubAuthentik(t *testing.T) *stubAuthentik { }) mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { _ = r.ParseForm() + + // Refresh grant: hand back a rotated access token (and a fresh + // id_token) without re-running the PKCE dance. Used by the + // TokenSource renewal tests. + if r.Form.Get("grant_type") == "refresh_token" { + clientID := r.Form.Get("client_id") + if clientID == "" { + if user, _, ok := r.BasicAuth(); ok { + clientID = user + } + } + if clientID == "" { + clientID = "test-client" + } + idToken := stub.signIDToken(t, clientID, map[string]any{ + "sub": "user-1", + "email": "ops@example.com", + "name": "Test Operator", + }) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "access_token": "at-refreshed", + "refresh_token": "rt-2", + "id_token": idToken, + "token_type": "Bearer", + "expires_in": 3600, + "refresh_expires_in": 86400, + }) + return + } + code := r.Form.Get("code") verifier := r.Form.Get("code_verifier") stub.mu.Lock() diff --git a/internal/authn/source.go b/internal/authn/source.go new file mode 100644 index 0000000..984de5d --- /dev/null +++ b/internal/authn/source.go @@ -0,0 +1,234 @@ +//go:build unix + +package authn + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "time" + + "gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring" +) + +// minRenewInterval bounds how often Run attempts a renewal, even when +// the access-token lifetime is very short (the sherlock-cli provider +// issues ~5-minute tokens). Prevents a hot loop on sub-minute TTLs. +const minRenewInterval = 5 * time.Second + +// renewBackoff is how long Run waits after a failed refresh before +// re-evaluating. Refresh failures are usually transient (a network +// blip); the wallet still holds the soon-to-expire token in the +// meantime. +const renewBackoff = 15 * time.Second + +// OnToken is the callback every MCP must supply to a TokenSource. The +// source invokes it once with the initial token (from Start) and again +// on every renewal (from Run), each call on the source's goroutine. +// +// Implementations MUST be safe to call concurrently with the MCP's +// request path — the token rotates underneath in-flight work. The +// canonical implementation is TokenHolder.Set (see below), which the +// request path then reads lock-free via TokenHolder.AccessToken. +type OnToken func(keyring.TokenSet) + +// TokenSource keeps one service's access token fresh and pushes every +// new token to the MCP via a mandatory callback. +// +// The problem it solves: Authentik access tokens are short-lived +// (~5 min for the sherlock-cli provider). An MCP that captured a token +// once at startup would start getting 401s mid-session. A TokenSource +// renews the token before it expires and hands each new one to the +// MCP's callback, so the MCP never has to poll or reason about expiry. +// +// Lifecycle: +// +// src := authn.NewTokenSource(store, "grafana", cfg, opts, holder.Set) +// ts, err := src.Start(ctx) // initial OAuth (browser on first use) +// go src.Run(ctx) // renews + calls the callback forever +type TokenSource struct { + store keyring.Store + service string + cfg Config + opts EnsureOptions + onToken OnToken + + // skew/minWait/clock are tunable for tests; production uses the + // package defaults. + skew time.Duration + minWait time.Duration + clock func() time.Time + + mu sync.Mutex + current keyring.TokenSet +} + +// NewTokenSource builds a TokenSource for service. cfg/opts mirror what +// Ensure takes. Both opts.LockPath and onToken are required — a missing +// lock path or a nil callback is always a wiring bug, so we panic +// rather than silently degrade. +func NewTokenSource(store keyring.Store, service string, cfg Config, opts EnsureOptions, onToken OnToken) *TokenSource { + if opts.LockPath == "" { + panic("authn: NewTokenSource requires opts.LockPath") + } + if onToken == nil { + panic("authn: NewTokenSource requires a non-nil onToken callback") + } + return &TokenSource{ + store: store, + service: service, + cfg: cfg, + opts: opts, + onToken: onToken, + skew: RefreshSkew, + minWait: minRenewInterval, + clock: time.Now, + } +} + +// Start performs the initial token acquisition, running a full OAuth +// flow if the wallet is empty (this is the call that may open a +// browser). It caches the token and invokes the callback with it, then +// returns it so callers can do a one-shot check (e.g. --probe). Call +// Start once, before Run. +func (s *TokenSource) Start(ctx context.Context) (keyring.TokenSet, error) { + ts, err := Ensure(ctx, s.store, s.service, s.cfg, s.opts) + if err != nil { + return keyring.TokenSet{}, err + } + s.mu.Lock() + s.current = ts + s.mu.Unlock() + s.onToken(ts) + return ts, nil +} + +// Run renews the token ahead of expiry until ctx is cancelled, +// invoking the callback on every rotation. It performs refresh-only +// renewals (it never opens a browser); if the refresh token has itself +// expired the renewal fails and Run backs off, leaving re-login to the +// operator (next sherlock invocation). Returns ctx.Err() on exit. +func (s *TokenSource) Run(ctx context.Context) error { + for { + timer := time.NewTimer(s.nextWait()) + select { + case <-ctx.Done(): + timer.Stop() + return ctx.Err() + case <-timer.C: + } + + if err := s.renew(ctx); err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + // Transient failure: bounded backoff, then re-evaluate. + b := time.NewTimer(renewBackoff) + select { + case <-ctx.Done(): + b.Stop() + return ctx.Err() + case <-b.C: + } + } + } +} + +// nextWait computes how long to sleep before the next proactive +// renewal: skew before the cached token expires, clamped to minWait so +// a very short TTL doesn't spin. +func (s *TokenSource) nextWait() time.Duration { + s.mu.Lock() + cur := s.current + s.mu.Unlock() + if cur.Empty() { + return s.minWait + } + wait := cur.IDExpiresAt.Sub(s.clock()) - s.skew + if wait < s.minWait { + return s.minWait + } + return wait +} + +// renew refreshes the token under the cross-process flock and, if the +// access token actually rotated, pushes it to the callback. Never opens +// a browser. +func (s *TokenSource) renew(ctx context.Context) error { + fresh, err := EnsureFresh(ctx, s.store, s.service, EnsureFreshOptions{ + LockPath: s.opts.LockPath, + Discoverer: s.opts.Discoverer, + Clock: s.clock, + }) + if err != nil { + return err + } + s.mu.Lock() + changed := fresh.AccessToken != s.current.AccessToken + s.current = fresh + s.mu.Unlock() + if changed { + s.onToken(fresh) + } + return nil +} + +// Current returns a snapshot of the cached TokenSet without any I/O. +func (s *TokenSource) Current() keyring.TokenSet { + s.mu.Lock() + defer s.mu.Unlock() + return s.current +} + +// errNoToken is returned by a TokenHolder accessor when no token has +// been delivered yet (should not happen after Start). +var errNoToken = errors.New("authn: no token delivered yet") + +// TokenHolder is the canonical MCP-side glue between a TokenSource's +// push callback and a request path that needs the current bearer. Its +// Set method is the OnToken callback; AccessToken / Bearer are the +// lock-free reads the request path uses. +// +// holder := authn.NewTokenHolder() +// src := authn.NewTokenSource(store, svc, cfg, opts, holder.Set) +// ... // request path: req.Header.Set("Authorization", holder.Bearer()) +type TokenHolder struct { + v atomic.Pointer[keyring.TokenSet] +} + +// NewTokenHolder returns an empty holder. +func NewTokenHolder() *TokenHolder { return &TokenHolder{} } + +// Set stores ts as the current token. It satisfies OnToken and is safe +// for concurrent use. +func (h *TokenHolder) Set(ts keyring.TokenSet) { + cp := ts + h.v.Store(&cp) +} + +// Current returns the last token delivered, or a zero TokenSet. +func (h *TokenHolder) Current() keyring.TokenSet { + if p := h.v.Load(); p != nil { + return *p + } + return keyring.TokenSet{} +} + +// AccessToken returns the current access token, or "" if none yet. +func (h *TokenHolder) AccessToken() string { + if p := h.v.Load(); p != nil { + return p.AccessToken + } + return "" +} + +// Bearer returns a ready-to-use "Bearer " header value, or an +// error if no token has been delivered yet. +func (h *TokenHolder) Bearer() (string, error) { + tok := h.AccessToken() + if tok == "" { + return "", errNoToken + } + return "Bearer " + tok, nil +} diff --git a/internal/authn/source_test.go b/internal/authn/source_test.go new file mode 100644 index 0000000..e7de013 --- /dev/null +++ b/internal/authn/source_test.go @@ -0,0 +1,173 @@ +//go:build unix + +package authn + +import ( + "context" + "sync" + "testing" + "time" + + "gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring" + fakekeyring "gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring/fake" +) + +func TestNewTokenSource_RequiresCallback(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("expected panic on nil onToken") + } + }() + NewTokenSource(fakekeyring.New(), "gitea", + Config{Issuer: "https://x", ClientID: "y"}, + EnsureOptions{LockPath: t.TempDir() + "/lock"}, nil) +} + +func TestNewTokenSource_RequiresLockPath(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("expected panic on empty LockPath") + } + }() + NewTokenSource(fakekeyring.New(), "gitea", + Config{Issuer: "https://x", ClientID: "y"}, + EnsureOptions{}, func(keyring.TokenSet) {}) +} + +func TestTokenSource_Start_DeliversInitialToken(t *testing.T) { + store := fakekeyring.New() + _ = store.Set("gitea", keyring.TokenSet{ + IDToken: "fresh", + AccessToken: "at-0", + IDExpiresAt: time.Now().Add(time.Hour), + Issuer: "x", + ClientID: "y", + }) + + var got []string + var mu sync.Mutex + cb := func(ts keyring.TokenSet) { + mu.Lock() + got = append(got, ts.AccessToken) + mu.Unlock() + } + + src := NewTokenSource(store, "gitea", + Config{Issuer: "https://example.test", ClientID: "y"}, + EnsureOptions{LockPath: t.TempDir() + "/lock"}, cb) + + ts, err := src.Start(context.Background()) + if err != nil { + t.Fatalf("Start: %v", err) + } + if ts.AccessToken != "at-0" { + t.Fatalf("Start returned %q, want at-0", ts.AccessToken) + } + mu.Lock() + defer mu.Unlock() + if len(got) != 1 || got[0] != "at-0" { + t.Fatalf("callback got %v, want [at-0]", got) + } +} + +func TestTokenSource_Run_RenewsAndNotifies(t *testing.T) { + stub := newStubAuthentik(t) + store := fakekeyring.New() + // Seed a STALE token (already expired) with a refresh token so the + // renewal path fires immediately. + _ = store.Set("gitea", keyring.TokenSet{ + IDToken: "old", + AccessToken: "at-stale", + RefreshToken: "rt-1", + IDExpiresAt: time.Now().Add(-time.Minute), + Issuer: stub.srv.URL, + ClientID: "test-client", + Scopes: []string{"openid"}, + }) + + tokens := make(chan string, 4) + src := NewTokenSource(store, "gitea", + Config{Issuer: stub.srv.URL, ClientID: "test-client"}, + EnsureOptions{LockPath: t.TempDir() + "/lock"}, + func(ts keyring.TokenSet) { tokens <- ts.AccessToken }) + src.minWait = time.Millisecond // refresh almost immediately + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + done := make(chan struct{}) + go func() { _ = src.Run(ctx); close(done) }() + + select { + case got := <-tokens: + if got != "at-refreshed" { + t.Fatalf("renewed token = %q, want at-refreshed", got) + } + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for renewal callback") + } + + // The rotated token must be persisted to the wallet too. + stored, err := store.Get("gitea") + if err != nil { + t.Fatalf("Get: %v", err) + } + if stored.AccessToken != "at-refreshed" { + t.Fatalf("wallet access token = %q, want at-refreshed", stored.AccessToken) + } + + cancel() + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("Run did not exit after cancel") + } +} + +func TestTokenSource_Run_StopsOnCancel(t *testing.T) { + store := fakekeyring.New() + _ = store.Set("gitea", keyring.TokenSet{ + IDToken: "fresh", + AccessToken: "at-0", + IDExpiresAt: time.Now().Add(time.Hour), + Issuer: "x", + ClientID: "y", + }) + src := NewTokenSource(store, "gitea", + Config{Issuer: "https://example.test", ClientID: "y"}, + EnsureOptions{LockPath: t.TempDir() + "/lock"}, + func(keyring.TokenSet) {}) + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- src.Run(ctx) }() + cancel() + select { + case err := <-done: + if err != context.Canceled { + t.Fatalf("Run returned %v, want context.Canceled", err) + } + case <-time.After(2 * time.Second): + t.Fatal("Run did not return after cancel") + } +} + +func TestTokenHolder(t *testing.T) { + h := NewTokenHolder() + if h.AccessToken() != "" { + t.Fatal("empty holder should return empty access token") + } + if _, err := h.Bearer(); err == nil { + t.Fatal("empty holder Bearer should error") + } + h.Set(keyring.TokenSet{AccessToken: "at-1"}) + if h.AccessToken() != "at-1" { + t.Fatalf("AccessToken = %q, want at-1", h.AccessToken()) + } + if b, err := h.Bearer(); err != nil || b != "Bearer at-1" { + t.Fatalf("Bearer = %q, %v", b, err) + } + h.Set(keyring.TokenSet{AccessToken: "at-2"}) + if h.AccessToken() != "at-2" { + t.Fatalf("AccessToken = %q, want at-2 after update", h.AccessToken()) + } +} diff --git a/plan.md b/plan.md index c585a3d..caaf852 100644 --- a/plan.md +++ b/plan.md @@ -238,6 +238,8 @@ Reuses the existing gssh server (`gssh.alexandru.macocian.me`) — sherlock does - **Loopback redirect port:** `127.0.0.1:6990` (unassigned per IANA). Bound only for the duration of an `authn.Login` call (i.e. when an MCP triggers a first-time PKCE flow). Each Authentik provider sherlock talks to must whitelist this URI. End-to-end flow is exercised by an integration test against a stub Authentik (`internal/authn/login_test.go`). - **No daemon (reverses Phase 1 #9 + #10):** the Phase 1 design had a `sherlock-broker` daemon with UDS RPC, JSON-over-newline framing, fork+setsid lifecycle, PID-file flock, and idle-timeout watcher. It was scrapped — see [docs/architecture.md#why-there-is-no-daemon-and-no-sherlock-login](docs/architecture.md#why-there-is-no-daemon-and-no-sherlock-login). Multi-process refresh races are serialised via an exclusive `flock()` on `$XDG_RUNTIME_DIR/sherlock.refresh.lock` in `internal/authn/refresh.go`. - **No `sherlock login` (reverses earlier Phase 1 plan):** preemptive authentication doesn't match the topology. Caddy + each downstream service each have their own Authentik OAuth providers with their own audiences, scopes, and consent screens; one master session can't satisfy all of them. Authentication is lazy and per-MCP: each MCP at startup calls `authn.Ensure(store, "", cfg, opts)`, which reads the wallet, refreshes if stale, or runs a fresh PKCE flow on miss. The sherlock CLI surface is reduced to `status`, `logout []`, `` / `run `, `version`, `help`. See [docs/auth-model.md](docs/auth-model.md). +- **Grafana MCP shape:** `cmd/grafana-mcp` imports Grafana Labs' upstream `mcp-grafana` as a Go package and serves its read-only tools in-process (no external binary, no `uvx`, no `exec`). Auth is sherlock's lazy `authn` OAuth; the operator's bearer is injected per request via `GrafanaConfig.BaseTransport` and kept fresh by the `authn.TokenSource` renewer. It is registered by default and requires Grafana-side `[auth.jwt]` validating the `sherlock-cli` Authentik JWT (added to `Charlie/victoriametrics`). See [docs/grafana-mcp.md](docs/grafana-mcp.md). +- **Token renewal (all MCPs):** Authentik access tokens are short-lived (~5 min for `sherlock-cli`). `authn.TokenSource` runs a background refresh loop and pushes every renewed token to a **mandatory MCP-supplied callback** (`OnToken`); MCPs wire `authn.TokenHolder.Set` as the callback and read the current bearer lock-free from the holder. No MCP holds a static token. See [docs/auth-model.md](docs/auth-model.md#token-renewal). ## Still open