feat: wire Infisical secret store into API server and MCP tools
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

- Wire secretsManager in NewHandler() — instantiate InfisicalBackend
  when OIKOS_INFISICAL_SITE_URL is set (previously always nil)
- Add get_secret, list_secrets, set_secret MCP tools with nil-backend
  graceful degradation
- Add oikos secret get|set|list CLI subcommands for Infisical
- Fix Set() bug: create-before-update so new keys are created;
  add Type: "shared" to Update so it finds the right secret;
  disable SDK cache so Get returns fresh data after Set
- Clean enrollment response: remove fake infisical_client_id/
  infisical_client_secret stubs, store age key in Infisical for real
This commit is contained in:
2026-08-05 23:03:27 +02:00
parent 38c472a118
commit e3449b24c1
12 changed files with 590 additions and 241 deletions

View File

@@ -28,6 +28,7 @@ import (
"github.com/dtoro/oikos/internal/httpapi/gen"
mcphandler "github.com/dtoro/oikos/internal/mcp"
"github.com/dtoro/oikos/internal/safego"
"github.com/dtoro/oikos/internal/secrets"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
@@ -59,8 +60,11 @@ type Server struct {
}
// secretsBackend is a minimal interface for secrets operations used by the
// HTTP API (enrollment key storage, listing). Compatible with internal/secrets.
// HTTP API (enrollment key storage, listing, retrieval). Compatible with
// internal/secrets.Backend. If Infisical is configured, a real backend is
// wired in; otherwise the field stays nil and all guarded paths are no-ops.
type secretsBackend interface {
Get(ctx context.Context, key string) (string, error)
Set(ctx context.Context, key string, value string) error
List(ctx context.Context) ([]string, error)
}
@@ -80,6 +84,23 @@ func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Hand
sseSubs: make(map[*sseSubscriber]struct{}),
}
// Wire Infisical backend when configured.
if cfg.InfisicalSiteURL != "" {
infCfg := secrets.InfisicalConfig{
SiteURL: cfg.InfisicalSiteURL,
ClientID: cfg.InfisicalClientID,
ClientSecret: cfg.InfisicalClientSecret,
ProjectID: cfg.InfisicalProjectID,
SecretPath: "/",
Env: cfg.InfisicalEnv,
}
if infCfg.Env == "" {
infCfg.Env = "dev"
}
s.secretsManager = secrets.NewInfisicalBackend(infCfg)
slog.Info("secrets backend wired", "backend", "infisical", "site", cfg.InfisicalSiteURL)
}
// Start background SSE listener, tied to ctx for clean shutdown.
// handleNotification (called per-message inside sseListener's loop) has
// its own recover for the common case; this outer one covers the
@@ -271,7 +292,7 @@ func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Hand
if nomosAgentID == uuid.Nil && cfg.NomosAgentSlug != "" {
_ = pool.QueryRow(ctx, "SELECT id FROM entities WHERE slug = $1", cfg.NomosAgentSlug).Scan(&nomosAgentID)
}
r.With(combinedAuth(cfg, false)).Handle("/mcp", mcphandler.NewHandler(pool, cfg.MCPBearerToken, nomosAgentID))
r.With(combinedAuth(cfg, false)).Handle("/mcp", mcphandler.NewHandler(pool, cfg.MCPBearerToken, nomosAgentID, s.secretsManager))
if nomosURL := os.Getenv("NOMOS_PROXY_URL"); nomosURL != "" {
target, _ := url.Parse(nomosURL)