feat: migrate config system from dotenv to koanf/YAML#102
Conversation
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
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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.
This comment has been minimized.
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)
This comment has been minimized.
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.
This comment has been minimized.
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.
This reverts commit a6e82a5.
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 |
There was a problem hiding this comment.
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.
This comment has been minimized.
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 |
There was a problem hiding this comment.
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.
This comment has been minimized.
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.
|
Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.
Or push these changes by commenting: 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.
| } | ||
| return strings.ToLower(key), value | ||
| }) | ||
| _ = k.Load(envProvider, nil) |
There was a problem hiding this comment.
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.



Summary
Migrate server configuration from
godotenv/.env files tokoanfwith nested YAML config files, enabling a zero-config local installation experience.Companion PR
What changed
Config system (
cmd/api/config/config.go):godotenvwithkoanffor config loadingConfigstruct 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/hypeman/config.yamlon macOS,/etc/hypeman/config.yamlon Linux)CONFIG_PATHenv var; errors if explicitly specified path is missingEnvironment variable convention:
PORT,DATA_DIR,JWT_SECRET,ENV__(double underscore) as separator:CADDY__LISTEN_ADDRESS,NETWORK__BRIDGE_NAME,OTEL__ENABLED, etc.CADDY_LISTEN_ADDRESS,BRIDGE_NAME) no longer work — use the__convention or switch toconfig.yamlToken tool (
cmd/gen-jwt/main.go):jwt_secretdirectly from server config.yaml (no wrapper scripts)CONFIG_PATHenv var, same as the server-durationflag for configurable token expiryJWT_SECRETenv var still works as highest-precedence overrideInstall script (
scripts/install.sh):config.yamlinstead of dotenv-style config files~/.config/hypeman/cli.yamlwith pre-authenticated long-lived tokenhypemanCLI andhypeman-tokenCONFIG_PATHjwt_secretparsing from existing configs (handles whitespace, quotes)Example files:
config.example.yaml(Linux),config.example.darwin.yaml(macOS),cli.example.yaml.env.exampleand.env.darwin.exampleCodebase-wide refactor:
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 devstarts successfully reading fromconfig.yamlhypeman-tokengenerates tokens withoutJWT_SECRETenv var (reads from config.yaml)CADDY__LISTEN_ADDRESS=127.0.0.1overridescaddy.listen_addressPORT=9090overridesport: 8080CONFIG_PATH=/custom/path.yamlworks; missing path returns errorscripts/install.shgenerates valid nestedconfig.yamlandcli.yamlNote
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
.envfiles to nested YAML config loaded viakoanf, with defaults + optionalCONFIG_PATHsearch and env overrides using__for nesting; code is refactored across providers, network, resources, and tests to read from the new structuredConfigfields.Updates install/build tooling to be zero-config after install:
scripts/install.shnow generatesconfig.yamlwith a randomjwt_secret, sets services to passCONFIG_PATH(instead ofEnvironmentFile), installs/symlinkshypeman/hypeman-token(no wrapper scripts), and generates~/.config/hypeman/cli.yamlwith a long-lived token.hypeman-tokennow readsjwt_secretfrom 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.