Skip to content

feat: migrate config system from dotenv to koanf/YAML#102

Merged
rgarcia merged 20 commits intomainfrom
koanf-yaml-config
Feb 16, 2026
Merged

feat: migrate config system from dotenv to koanf/YAML#102
rgarcia merged 20 commits intomainfrom
koanf-yaml-config

Conversation

@rgarcia
Copy link
Contributor

@rgarcia rgarcia commented Feb 15, 2026

Summary

Migrate server configuration from godotenv/.env files to koanf with nested YAML config files, enabling a zero-config local installation experience.

Companion PR

What changed

Config system (cmd/api/config/config.go):

  • Replaced godotenv with koanf for config loading
  • Config struct now uses nested sub-structs (e.g. CaddyConfig, NetworkConfig, ACMEConfig, OtelConfig, etc.) — config keys are organized hierarchically in both the Go struct and YAML files
  • Config precedence: env vars > YAML file > built-in defaults
  • Searches platform-specific paths (~/.config/hypeman/config.yaml on macOS, /etc/hypeman/config.yaml on Linux)
  • Explicit path via CONFIG_PATH env var; errors if explicitly specified path is missing

Environment variable convention:

  • Top-level keys: PORT, DATA_DIR, JWT_SECRET, ENV
  • Nested keys use __ (double underscore) as separator: CADDY__LISTEN_ADDRESS, NETWORK__BRIDGE_NAME, OTEL__ENABLED, etc.
  • Breaking change: old flat env var names (e.g. CADDY_LISTEN_ADDRESS, BRIDGE_NAME) no longer work — use the __ convention or switch to config.yaml

Token tool (cmd/gen-jwt/main.go):

  • Reads jwt_secret directly from server config.yaml (no wrapper scripts)
  • Respects CONFIG_PATH env var, same as the server
  • Added -duration flag for configurable token expiry
  • JWT_SECRET env var still works as highest-precedence override

Install script (scripts/install.sh):

  • Generates nested config.yaml instead of dotenv-style config files
  • Generates ~/.config/hypeman/cli.yaml with pre-authenticated long-lived token
  • Removed all wrapper scripts for both hypeman CLI and hypeman-token
  • Updated launchd/systemd service configs to use CONFIG_PATH
  • Robust jwt_secret parsing from existing configs (handles whitespace, quotes)

Example files:

  • Added config.example.yaml (Linux), config.example.darwin.yaml (macOS), cli.example.yaml
  • Removed .env.example and .env.darwin.example

Codebase-wide refactor:

  • Updated all config field access across lib/providers, lib/network, lib/resources, cmd/api/main.go, and all test files to use nested paths (e.g. cfg.Caddy.ListenAddress, cfg.Network.BridgeName)

Test plan

  • make dev starts successfully reading from config.yaml
  • hypeman-token generates tokens without JWT_SECRET env var (reads from config.yaml)
  • Nested env vars work: CADDY__LISTEN_ADDRESS=127.0.0.1 overrides caddy.listen_address
  • Top-level env vars still work: PORT=9090 overrides port: 8080
  • CONFIG_PATH=/custom/path.yaml works; missing path returns error
  • scripts/install.sh generates valid nested config.yaml and cli.yaml
  • e2e install test passes

Note

Medium Risk
Touches core startup configuration and installer/service wiring, with breaking changes to environment-variable names and config file formats that could prevent the server/CLI from starting if migration paths are missed.

Overview
Migrates server configuration from dotenv-style .env files to nested YAML config loaded via koanf, with defaults + optional CONFIG_PATH search and env overrides using __ for nesting; code is refactored across providers, network, resources, and tests to read from the new structured Config fields.

Updates install/build tooling to be zero-config after install: scripts/install.sh now generates config.yaml with a random jwt_secret, sets services to pass CONFIG_PATH (instead of EnvironmentFile), installs/symlinks hypeman/hypeman-token (no wrapper scripts), and generates ~/.config/hypeman/cli.yaml with a long-lived token. hypeman-token now reads jwt_secret from config files (and supports -duration), and CI/docs/e2e tests are updated to validate the new config flow and include new example YAML files (config.example*.yaml, cli.example.yaml).

Written by Cursor Bugbot for commit 304493e. This will update automatically on new commits. Configure here.

Replace godotenv-based .env config loading with koanf and YAML config
files across the entire stack:

Server (hypeman-api):
- Config struct now uses koanf tags for YAML unmarshaling
- Loads config.yaml from platform-specific paths with env var overrides
- CONFIG_PATH env var for explicit config file location

Token tool (hypeman-token):
- Reads jwt_secret directly from config.yaml (no more wrapper scripts)
- Added -duration flag for configurable token expiry

Install script:
- Generates config.yaml instead of dotenv-style config
- Generates ~/.config/hypeman/cli.yaml with pre-authenticated token
- Removed all wrapper scripts, replaced with symlinks on Linux
- Updated launchd/systemd service definitions

Also adds config.example.yaml, config.darwin.example.yaml, and
cli.example.yaml as reference templates.
- Pass JWT_SECRET explicitly to hypeman-token in install.sh (fixes
  Linux installs where config.yaml is root-only)
- Return error from config.Load() when explicit CONFIG_PATH fails
  instead of silently falling back to defaults
- Deduplicate config paths: gen-jwt now imports and uses
  config.GetDefaultConfigPaths() instead of maintaining its own copy
@cursor

This comment has been minimized.

@cursor

This comment has been minimized.

Use env.ProviderWithValue instead of env.Provider so that empty
environment variables (e.g. PORT="") don't override valid defaults
or YAML config values. This preserves the old getEnv() behavior.
@cursor

This comment has been minimized.

- hypeman-token now checks CONFIG_PATH env var before default paths,
  matching hypeman-api behavior for custom config locations
- install.sh uses $SUDO when reading jwt_secret from existing config
  file on Linux reinstalls (file is 640 root:root)
@cursor

This comment has been minimized.

…orkflow

- Make jwt_secret and port grep/sed pipelines handle leading whitespace,
  single/double quotes, trailing whitespace, and multiple matches
- Update make gen-jwt to auto-detect local config.yaml via CONFIG_PATH,
  restoring the dev workflow that previously relied on godotenv/.env

Addresses bugbot review comments on #102.
@cursor

This comment has been minimized.

- Replace explicit envKeyMap with koanf's __ delimiter for auto-mapping
  env vars to nested config paths (e.g. CADDY__LISTEN_ADDRESS -> caddy.listen_address)
- Rename config.darwin.example.yaml -> config.example.darwin.yaml for
  consistent naming
- Remove .env.example and .env.darwin.example references
- Update DEVELOPMENT.md to document __ convention and YAML-first config
- Update README.md configuration table

BREAKING: Old flat env var names (CADDY_LISTEN_ADDRESS, BRIDGE_NAME, etc.)
no longer work. Use double-underscore for nested keys (CADDY__LISTEN_ADDRESS,
NETWORK__BRIDGE_NAME) or configure via config.yaml instead.
README and DEVELOPMENT.md now document configuration using YAML key
names (dot notation for nested keys). Env var override convention
mentioned once as a footnote rather than being the primary reference.
The macOS BSD sed `a\` (append) command was inserting the docker_socket
line on the same line as builder_image, producing invalid YAML. Fix by:
1. Including docker_socket in example config templates so the simpler
   sed s| replacement path is used instead of sed a\.
2. Fixing the fallback sed a\ to use BSD-compatible s| with literal
   newline.
Also updates builder_image default to "none" in example files.
The released CLI (v0.11.0) doesn't support cli.yaml yet, so the e2e
test needs to export HYPEMAN_BASE_URL and HYPEMAN_API_KEY env vars.
Once the CLI release with koanf/yaml support ships, cli.yaml will
handle this and the env vars become redundant.
Add CLI_BRANCH env var to install.sh that clones and builds the CLI
from the specified branch of kernel/hypeman-cli instead of downloading
a release binary. Useful for testing unreleased CLI features.

The e2e test now passes CLI_BRANCH through to install.sh. Temporarily
set to koanf-yaml-config in CI so the e2e test uses the CLI with
cli.yaml config file support.
Missed gpu_module_test.go, gpu_e2e_test.go, and gpu_inference_test.go
during the config nesting migration. Uses config.NetworkConfig for
BridgeName, SubnetCIDR, and DNSServer fields.
run: brew list caddy &>/dev/null || brew install caddy
- name: Run E2E install test
run: bash scripts/e2e-install-test.sh
run: CLI_BRANCH=koanf-yaml-config bash scripts/e2e-install-test.sh
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI hardcodes temporary CLI branch name

High Severity

The e2e install test hardcodes CLI_BRANCH=koanf-yaml-config, referencing a development branch in the companion CLI repository. Once this PR and the companion CLI PR are merged and the koanf-yaml-config branch is deleted, the git clone --branch in install.sh will fail, breaking the CI e2e install test on every subsequent run.

Fix in Cursor Fix in Web

@cursor

This comment has been minimized.

run: brew list caddy &>/dev/null || brew install caddy
- name: Run E2E install test
run: bash scripts/e2e-install-test.sh
run: CLI_BRANCH=koanf-yaml-config bash scripts/e2e-install-test.sh
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: revert after CLI PR is merged / deployed

The inline heredoc configs duplicated defaults defined in the example
YAML files. Treat a failed config template download as a hard error
instead of silently generating a potentially stale config.
Without jwt_secret the server can't authenticate API requests, so
silently skipping it leaves the install in a broken state.
@rgarcia rgarcia requested a review from sjmiller609 February 15, 2026 23:53
@cursor

This comment has been minimized.

- Remove unreachable zero-value fallbacks in ProvideBuildManager that
  duplicated defaults already set by defaultConfig().
- Replace the "none" sentinel for builder_image with empty string.
  Empty string now canonically means "build from embedded Dockerfile
  on first run", which is simpler than a magic string.
@cursor
Copy link

cursor bot commented Feb 16, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Removed build config safety-net defaults risk zero values
    • Restored the safety-net checks for MaxConcurrentBuilds (default 2) and DefaultTimeout (default 600) that prevent explicitly-configured zero values in YAML from passing through to the build manager unchecked.

Create PR

Or push these changes by commenting:

@cursor push 1f410d6fa4
Preview (1f410d6fa4)
diff --git a/lib/providers/providers.go b/lib/providers/providers.go
--- a/lib/providers/providers.go
+++ b/lib/providers/providers.go
@@ -288,6 +288,16 @@
 		RegistrySecret:      cfg.JwtSecret, // Use same secret for registry tokens
 	}
 
+	// Safety-net: ensure critical build config values are never zero, even if
+	// explicitly set to 0 in YAML. A zero concurrent-builds limit would starve
+	// the build queue, and a zero timeout would expire builds immediately.
+	if buildConfig.MaxConcurrentBuilds == 0 {
+		buildConfig.MaxConcurrentBuilds = 2
+	}
+	if buildConfig.DefaultTimeout == 0 {
+		buildConfig.DefaultTimeout = 600
+	}
+
 	// Configure secret provider (use NoOpSecretProvider as fallback to avoid nil panics)
 	var secretProvider builds.SecretProvider
 	if cfg.Build.SecretsDir != "" {

Reject zero/negative values at startup rather than passing them through
to the build manager where they'd cause immediate timeouts or a
zero-capacity semaphore that blocks all builds.
@rgarcia rgarcia merged commit 0a8b795 into main Feb 16, 2026
6 checks passed
@rgarcia rgarcia deleted the koanf-yaml-config branch February 16, 2026 13:43
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before Autofix could start.

}
return strings.ToLower(key), value
})
_ = k.Load(envProvider, nil)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Env provider without prefix can crash on colliding env vars

Medium Severity

The env.ProviderWithValue uses an empty prefix "", which causes koanf to read all system environment variables. If any env var name (lowercased, without __) matches a sub-struct config key — such as REGISTRY, BUILD, NETWORK, GPU, API, LOGGING, or CADDY — the scalar string value replaces the existing nested config map via maps.Merge. This causes k.Unmarshal to fail with "expected a map, got 'string'", preventing the application from starting. The old explicit getEnv() approach only read specifically named variables and was immune to this. Adding a prefix (e.g., HYPEMAN_) to filter env vars would prevent accidental collisions.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants