Skip to content

oneOf-of-submodules for secrets.providers fails nix flake check #70

@dansanduleac

Description

@dansanduleac

Summary

secrets.providers in nix/generated/openclaw-config-options.nix uses t.oneOf with three t.submodule variants (discriminated by source). This fails nix flake check — Nix's types.oneOf cannot reliably discriminate between submodules that share overlapping option names.

As a result, it's impossible to declare secret providers declaratively (except with source = "env"):

programs.openclaw.instances.default.config.secrets.providers.my-secrets = {
  source = "file";
  path = "/run/secrets/openclaw_json";
  mode = "json";
};

Root Cause

The generated type for secrets.providers (around line 10149) is:

providers = lib.mkOption {
  type = t.nullOr (t.attrsOf (t.oneOf [
    (t.submodule { options = { source = lib.mkOption { type = t.enum [ "env" ]; }; ... }; })
    (t.submodule { options = { source = lib.mkOption { type = t.enum [ "file" ]; }; ... }; })
    (t.submodule { options = { source = lib.mkOption { type = t.enum [ "exec" ]; }; ... }; })
  ]));
  default = null;
};

types.oneOf checks each variant in sequence via its check function, but types.submodule's check is very permissive — it accepts any attrset. This means:

  1. Multiple variants match the input value.
  2. The merge function then fails because the value doesn't satisfy all variants simultaneously.
  3. nix flake check (which evaluates all options strictly) surfaces the error, even when the value is perfectly valid for one specific variant.

This is a known limitation of types.oneOf with submodules in nixpkgs — it works for structurally distinct types (e.g., str vs submodule) but not for discriminated-union-style submodules that share keys like source.
Same issue was also flagged here on nixpkgs: NixOS/nixpkgs#337108

Suggested Fix

Instead of generating t.oneOf [ submoduleA submoduleB submoduleC ], the code generator should produce a single t.submodule where:

  • source remains the discriminator as t.enum [ "env" "file" "exec" ]
  • Each variant's fields are nested under a submodule named after the variant, eliminating any overlap between variants

For example:

providers = lib.mkOption {
  type = t.nullOr (t.attrsOf (t.submodule { options = {
    source = lib.mkOption { type = t.enum [ "env" "file" "exec" ]; };
    env = lib.mkOption {
      type = t.nullOr (t.submodule { options = {
        allowlist = lib.mkOption { type = t.nullOr (t.listOf t.str); default = null; };
      }; });
      default = null;
    };
    file = lib.mkOption {
      type = t.nullOr (t.submodule { options = {
        path = lib.mkOption { type = t.str; };
        mode = lib.mkOption { type = t.nullOr (t.enum [ "singleValue" "json" ]); default = null; };
        maxBytes = lib.mkOption { type = t.nullOr t.int; default = null; };
        timeoutMs = lib.mkOption { type = t.nullOr t.int; default = null; };
      }; });
      default = null;
    };
    exec = lib.mkOption {
      type = t.nullOr (t.submodule { options = {
        command = lib.mkOption { type = t.str; };
        args = lib.mkOption { type = t.nullOr (t.listOf t.str); default = null; };
        # ... remaining exec-only options
      }; });
      default = null;
    };
  }; }));
  default = null;
};

Usage would then look like:

secrets.providers.my-secrets = {
  source = "file";
  file = {
    path = "/run/secrets/openclaw_json";
    mode = "json";
  };
};

This keeps each variant's options cleanly separated with no field overlap, preserves full type safety (required fields like path and command stay non-nullable), and is idiomatic in the Nix module system.

The relevant place to fix this is likely in nix/scripts/generate-config-options.ts, in the oneOf handler — when all entries are submodules sharing a discriminator field, generate a single submodule with nested variant submodules instead of emitting t.oneOf.

Current Workaround

Patch the generated config JSON in a home.activation script after nix-openclaw writes it, using jq to inject secrets.providers outside of the Nix module system:

home.activation.openclawSecretsProviders =
  lib.hm.dag.entryAfter [ "openclawConfigFiles" ] ''
    config="$HOME/.openclaw/openclaw.json"
    if [ -e "$config" ]; then
      resolved="$(readlink -f "$config")"
      ${lib.getExe pkgs.jq} '
        .secrets.providers["sops"] = {"source": "file", "path": "/run/secrets/openclaw_json", "mode": "json"}
      ' "$resolved" >| "$config.tmp"
      run --quiet mv "$config.tmp" "$config"
    fi
  '';

Related

Environment

  • nix-openclaw rev: 2b1e71bd01ee7984649ab18a4eaa76ada29d4575
  • nixpkgs: NixOS 24.11
  • Platform: x86_64-linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions