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

@@ -115,7 +115,7 @@ Roles:
scheduler Run the observe loop
notifier Run the notification service (Matrix alerts)
all Run all roles in one process (dev mode)
secret Secret management (Infisical)
secret Secret management (Infisical: get, set, list, migrate, export-sops)
knowledge Convert wiki to knowledge seed (one-shot)
version Print version info
@@ -289,20 +289,45 @@ func runWithPool(ctx context.Context, cfg config.Config, name string, fn func(co
func runSecret(ctx context.Context, cfg config.Config) {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: oikos secret <list|migrate|export-sops>")
fmt.Fprintln(os.Stderr, "usage: oikos secret <get|set|list|migrate|export-sops>")
os.Exit(1)
}
sub := os.Args[2]
secretsDir := cfg.SecretsDir
if secretsDir == "" {
secretsDir = "archive/secrets-sops-backup"
}
sopsBackend := secrets.NewSOPSBackend(secretsDir)
// For get/set/list: use Infisical directly
switch sub {
case "get":
if len(os.Args) < 4 {
fmt.Fprintln(os.Stderr, "usage: oikos secret get <key>")
os.Exit(1)
}
key := os.Args[3]
backend := newInfisicalBackendOrFail(cfg)
val, err := backend.Get(ctx, key)
if err != nil {
slog.Error("secret get", "key", key, "error", err)
os.Exit(1)
}
fmt.Println(val)
case "set":
if len(os.Args) < 5 {
fmt.Fprintln(os.Stderr, "usage: oikos secret set <key> <value>")
os.Exit(1)
}
key := os.Args[3]
value := os.Args[4]
backend := newInfisicalBackendOrFail(cfg)
if err := backend.Set(ctx, key, value); err != nil {
slog.Error("secret set", "key", key, "error", err)
os.Exit(1)
}
fmt.Printf("stored: %s\n", key)
case "list":
keys, err := sopsBackend.List(ctx)
backend := newInfisicalBackendOrFail(cfg)
keys, err := backend.List(ctx)
if err != nil {
slog.Error("secret list", "error", err)
os.Exit(1)
@@ -311,24 +336,46 @@ func runSecret(ctx context.Context, cfg config.Config) {
fmt.Println(k)
}
case "migrate":
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"
}
if infCfg.SiteURL == "" {
fmt.Fprintln(os.Stderr, "error: OIKOS_INFISICAL_SITE_URL not set")
os.Exit(1)
}
case "migrate", "export-sops":
runSecretLegacy(ctx, cfg, sub)
infBackend := secrets.NewInfisicalBackend(infCfg)
default:
fmt.Fprintf(os.Stderr, "unknown secret command: %s\n", sub)
os.Exit(1)
}
}
// newInfisicalBackendOrFail creates an Infisical backend from config or exits.
func newInfisicalBackendOrFail(cfg config.Config) *secrets.InfisicalBackend {
if cfg.InfisicalSiteURL == "" {
fmt.Fprintln(os.Stderr, "error: OIKOS_INFISICAL_SITE_URL not set")
os.Exit(1)
}
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"
}
return secrets.NewInfisicalBackend(infCfg)
}
// runSecretLegacy handles SOPS-only commands (migrate, export-sops).
func runSecretLegacy(ctx context.Context, cfg config.Config, sub string) {
secretsDir := cfg.SecretsDir
if secretsDir == "" {
secretsDir = "archive/secrets-sops-backup"
}
sopsBackend := secrets.NewSOPSBackend(secretsDir)
switch sub {
case "migrate":
infBackend := newInfisicalBackendOrFail(cfg)
keys, err := sopsBackend.List(ctx)
if err != nil {
slog.Error("migrate: read sops", "error", err)
@@ -366,10 +413,6 @@ func runSecret(ctx context.Context, cfg config.Config) {
fmt.Printf("%s: <sops-encrypted>\n", k)
}
fmt.Printf("\n# To restore: sops -d secrets/*.yaml\n")
default:
fmt.Fprintf(os.Stderr, "unknown secret command: %s\n", sub)
os.Exit(1)
}
}