Files
Anand 22c1dd382c Add API keys, MCP server, admin AI providers, and a built-in local LLM option
- User-scoped API keys (Profile > API Keys) for 3rd-party REST API access
  and MCP clients, each locked to one scope at creation, with expiry,
  revocation, and last-used tracking.
- A hand-rolled MCP (Model Context Protocol) server exposing the fleet
  (connections, nodes, guests, storage, pools, alerts, cluster status) as
  read tools plus one admin-gated power-action tool, so Claude Code/Desktop
  or any other MCP client can query and operate the fleet directly.
- Both the REST API and MCP are off by default and toggleable instance-wide
  from Settings > API & MCP, enforced live on every request.
- Admin-managed AI providers (any OpenAI-chat-completions-compatible
  endpoint) backing the AI Assistant's tool-calling loop, replacing the
  single hardcoded provider.
- A built-in, zero-config, no-API-key local provider backed by Needle 2
  (internal/needle) for fully offline tool-calling, wired in as a one-click
  preset. Requires the operator to separately download the Needle 2 binary
  and point FERRUM_NEEDLE_BIN at it -- Ferrum never fetches executable
  content from the network itself; see README "Built-in LLM (Needle 2)".
- System settings (CORS allow-list, instance-wide toggles) moved to the
  admin Settings UI; environment variables are now scoped to true
  bootstrap-level config only (listen address, TLS, DB connection, secret,
  optional Needle binary path).
- Fixed: node Journal tab 502'ing with "unexpected end of JSON input" on an
  empty response, and separately with a decode error on PVE versions that
  return a bare-string journal line instead of the documented {n,t} object.
- Fixed: bottom content padding disappearing on every page except the AI
  Assistant (an unconditional h-full on the content wrapper let overflowing
  content bleed through where the padding should render).
- Fixed: Profile page felt cramped despite a wide viewport (stray max-w-2xl
  cap not present on the equivalent Settings page).
- Test coverage added for the previously-untested MCP package and the new
  Needle adapter (20 new Go tests), plus a regression test for the journal
  decode fix.
2026-09-06 13:26:30 +05:30

65 lines
1.8 KiB
Go

package api
import (
"context"
"database/sql"
"ferrum/internal/auth"
)
// BootstrapSettings loads OIDC and notification settings from the database
// and applies them (live OIDC client, live Notifier) at startup.
//
// seedOIDC is the config.yaml/env OIDC config (internal/config.OIDCConfig,
// converted by the caller) — config.yaml was the only way to set this up
// before the Settings UI existed. The very first boot after upgrading copies
// it into the database once; every boot after that (and every save from the
// UI) uses the database exclusively, so config.yaml stops being read for
// OIDC at all once a row exists.
func (s *Server) BootstrapSettings(ctx context.Context, seedEnabled bool, seedOIDC auth.OIDCConfig) error {
var exists int
err := s.db.QueryRowContext(ctx, `SELECT 1 FROM oidc_settings WHERE id = 1`).Scan(&exists)
if err == sql.ErrNoRows {
enc, encErr := s.secrets.Encrypt(seedOIDC.ClientSecret)
if encErr != nil {
return encErr
}
if err := s.saveOIDCRow(ctx, oidcRow{
enabled: seedEnabled, displayName: seedOIDC.DisplayName, issuerURL: seedOIDC.IssuerURL,
clientID: seedOIDC.ClientID, clientSecretEnc: enc, redirectURL: seedOIDC.RedirectURL,
allowAutoProvision: true, // matches the pre-existing always-on behavior config.yaml-only deployments already had
}); err != nil {
return err
}
} else if err != nil {
return err
}
row, err := s.loadOIDCRow(ctx)
if err != nil {
return err
}
s.applyOIDCRow(row)
notifRow, err := s.loadNotificationRow(ctx)
if err != nil {
return err
}
if err := s.applyNotificationRow(notifRow); err != nil {
return err
}
secRow, err := s.loadSecurityRow(ctx)
if err != nil {
return err
}
s.applySecurityRow(secRow)
sysRow, err := s.loadSystemRow(ctx)
if err != nil {
return err
}
s.applySystemRow(sysRow)
return nil
}