# DETAILS.md — Technical Reference Companion to [README.md](README.md). This file contains the full technical details that would normally live in a professional README — architecture, API surface, configuration schema, build system, and development workflows. Intended for developers, contributors, and automated tooling. --- ## Overview `obm` is a Go CLI that generates Terraform-compatible `.env` files through an interactive walkthrough or a YAML config file. It validates API credentials against live endpoints before writing config, and wraps Terraform lifecycle commands (init, apply, destroy). - **Language:** Go 1.22 - **Module:** `github.com/openboatmobile/obm` - **Dependencies:** `gopkg.in/yaml.v3` (sole external dependency) - **Binary:** Single statically-linked binary, zero runtime dependencies - **Current version:** See `VERSION` file (0.1.0 at time of writing) --- ## Architecture ``` obm/ ├── cmd/obm/main.go # CLI entry point, subcommand routing ├── internal/ │ ├── config/ │ │ ├── config.go # Config struct, GetValue/SetValue │ │ ├── deployment.go # DeploymentConfig, AdminUser(), MonthlyCostEstimate() │ │ ├── dotenv.go # DotEnvFile parser (round-trip .env read) │ │ ├── dotenv_writer.go # WriteDotEnv — grouped, commented .env output │ │ ├── schema.go # VarDef schema (all TF_VAR_ variables), VarGroup enum │ │ ├── tfvars.go # WriteTfVars — HCL-format tfvars output │ │ ├── yaml.go # YAMLConfig struct, LoadYAMLConfig(), ToDeploymentConfig() │ │ ├── config_test.go │ │ └── yaml_test.go │ ├── deploy/ │ │ └── deploy.go # Walkthrough orchestrator (Run, RunFromFile, RunWithConfig) │ ├── destroy/ │ │ ├── destroy.go # Terraform destroy with state parsing, confirmation │ │ └── destroy_test.go │ ├── inference/ │ │ ├── client.go # HTTP client, ValidateAPIKey(), ValidationResult │ │ ├── inference.go # Provider enum, ProviderConfig, FallbackChain, DefaultGLMConfig │ │ ├── client_test.go │ │ └── inference_test.go │ ├── prompt/ │ │ ├── prompt.go # Terminal I/O: Select, Confirm, Input, Password, color helpers │ │ └── prompt_test.go │ ├── provider/ │ │ ├── provider.go # Provider interface, BaseProvider, Registry, Register/Get │ │ ├── import.go # Provider registration (blank import guidance) │ │ ├── hetzner/ │ │ │ ├── hetzner.go # HetznerProvider: API validation, SSH key listing │ │ │ └── hetzner_test.go │ │ └── provider_test.go │ ├── terraform/ │ │ └── terraform.go # Runner: Init, Plan, Apply, Destroy wrappers │ └── validation/ │ ├── validation.go # Check interface, Runner, CheckResult, Status enum │ └── validation_test.go ├── scripts/ │ ├── install.sh # curl | sh installer │ └── release.sh # Tag + push release automation ├── .github/workflows/ │ ├── ci.yml # Test + build on push/PR │ └── release.yml # Cross-compile + GitHub Release on tag ├── Makefile # Build, test, lint, cross-compile targets ├── Dockerfile # Multi-stage: golang:1.22-alpine → alpine:3.20 ├── deploy.yaml.example # Full YAML config reference ├── CHANGELOG.md ├── CONTRIBUTING.md └── VERSION # Single line, e.g. "0.1.0" ``` --- ## CLI Interface ### Subcommands | Command | Flags | Description | |---------|-------|-------------| | `obm deploy` | `--config ` | Interactive walkthrough (default) or non-interactive from YAML | | `obm validate` | `--env-file ` | Load `.env`, check required vars, validate API keys | | `obm status` | — | Show deployment state (not yet implemented) | | `obm destroy` | — | Confirmation prompt → `terraform destroy` → state cleanup | | `obm version` | — | Print version with commit hash and build time | | `obm help` | — | Print usage | ### Build-time variables Injected via `-ldflags` at build time: | Variable | Flag | Example | |----------|------|---------| | `main.version` | `-X main.version=0.1.0` | Semver from `VERSION` file | | `main.gitCommit` | `-X main.gitCommit=abc1234` | Short commit SHA | | `main.buildTime` | `-X main.buildTime=2026-05-22T15:30:00Z` | UTC ISO timestamp | --- ## Deploy Walkthrough Flow The `obm deploy` interactive flow runs 8 steps in sequence: 1. **Framework** — Select Hermes or OpenClaw. Sets framework-specific defaults on `DeploymentConfig`. 2. **Cloud Provider** — Hetzner or DigitalOcean. 3. **Provider Token** — Enter API token. For Hetzner, validates against `/server_types` endpoint and lists SSH keys. 4. **SSH Key** — Select from keys found on the provider, or enter manually. 5. **Server Config** — Name, location/region, server type/droplet size. 6. **Inference Provider** — ZAI, Venice, OpenRouter. Enter API key. Validates against `/models` endpoint. 7. **Tailscale** — Optional VPN setup. Auth key and tailnet domain. 8. **Discord** — Optional bot integration. Bot token, server ID, user IDs. Final step: summary display with cost estimate → confirm → write `.env` → optionally run `terraform init && terraform apply`. ### Framework-specific defaults **Hermes:** - `DockerEnabled = true` - `VeniceBaseURL = "https://api.venice.ai/api/v1"` - `GatewayAllowAllUsers = true` - `DiscordAutoThread = true` **OpenClaw:** - `OpenClawVersion = "lts"` - `NodeVersion = "22"` - `EnableSwap = true`, `SwapSizeGB = 2` - `EnableFail2ban = true`, `EnableUnattendedUpgrades = true` --- ## Key Types ### DeploymentConfig (`internal/config/deployment.go`) Central struct holding all walkthrough choices. 30+ fields covering framework, provider, server, inference, Tailscale, Discord, and gateway configuration. Key methods: - `AdminUser() string` — returns framework name ("hermes" or "openclaw") - `MonthlyCostEstimate() string` — returns price string based on server type/droplet size Package-level helper functions (not methods, because `DeploymentConfig` is in `config` but called from `deploy`): - `config.LocationOrRegion(cfg)` — Hetzner location or DO region - `config.ServerTypeOrDroplet(cfg)` — server type or droplet size - `config.SSHKeySummary(cfg)` — masked SSH key display ### DotEnvFile (`internal/config/dotenv.go`) Round-trip parser for `.env` files: - `ParseDotEnv(path)` — parse `TF_VAR_`-prefixed env file - `env.GetVar(name)` — lookup with and without `TF_VAR_` prefix - `env.Values` — raw `map[string]string` ### VarDef Schema (`internal/config/schema.go`) Complete schema of all Terraform variables with metadata: ```go type VarDef struct { Name string // TF variable name (e.g. "cloud_provider") Type ValueType // string, number, bool, list Default string Required bool Sensitive bool Description string Group VarGroup // Section for organized output EnvComment string // Additional .env hint } ``` Variables are grouped: `PROVIDER`, `PROVIDER — Hetzner`, `PROVIDER — DigitalOcean`, `SERVER CONFIGURATION`, `SSH CONFIGURATION`, `API KEYS`, `MODEL CONFIGURATION`, `DISCORD`, `TAILSCALE`, `HERMES-SPECIFIC`, `OPENCLAW-SPECIFIC`, `SECURITY`, `PROJECT METADATA`. ### InferenceClient (`internal/inference/client.go`) HTTP client for validating inference API keys: - `ValidateAPIKey(ctx, provider, apiKey)` — hits `/models` endpoint, checks for HTTP 200 - Returns `ValidationResult{Valid, ErrorMessage, ModelCount, Latency}` - 30-second default timeout ### Provider Interface (`internal/provider/provider.go`) ```go type Provider interface { Name() string ProviderName() string Validate(ctx context.Context) error Checks(ctx context.Context) []validation.Check TokenEnvKey() string SetToken(token string) GetToken() string } ``` Provider registry pattern: `Register(name, factory)` / `Get(name)`. Hetzner implementation at `internal/provider/hetzner/`. ### Validation Framework (`internal/validation/validation.go`) Structured check system with `Runner`: ```go type Check interface { Name() string Category() CheckCategory Run(ctx context.Context) CheckResult } ``` Status values: `PASS`, `FAIL`, `WARN`, `SKIP`, `ERROR`. Categories: `Credentials`, `Connectivity`, `SSH Keys`, `Server Config`, `Quotas`, `Account`. --- ## Inference Providers Currently supported: | Provider | Enum | Base URL | Auth | |----------|------|----------|------| | Z.ai | `ProviderZAI` | `https://api.z.ai/api/coding/paas/v4` | `GLM_API_KEY` env | | Venice.ai | `ProviderVenice` | `https://api.venice.ai/api/v1` | `VENICE_API_KEY` env | | OpenRouter | `ProviderOpenRouter` | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` env | Fallback chains: ZAI → Venice → OpenRouter (for GLM models); Venice → OpenRouter. `DefaultGLMConfig()` sets `MaxTokens=16384` to prevent the over-compression bug where Venice defaults to 131K. --- ## .env Generation `WriteDotEnv()` in `internal/config/dotenv_writer.go` generates the `.env` file from a `Config`. Output format: - Header comment with usage instructions - Variables grouped by `VarGroup`, each with description comment - `TF_VAR_` prefix on all variable names - JSON arrays for SSH keys: `TF_VAR_ssh_key_names='["key-name"]'` (single-quoted shell string containing JSON) - Sensitive values get `YOUR_..._HERE` placeholders if empty - `WriteTfVars()` generates HCL-format `terraform.tfvars` as an alternative ### Variable flow User input → `DeploymentConfig` → `.env` (TF_VAR_ prefixed) → `source .env` → Terraform reads env vars → `templatefile()` → cloud-init → server provisioning. --- ## YAML Config (Non-interactive Mode) `LoadYAMLConfig(path)` parses a YAML file into `YAMLConfig`, then `ToDeploymentConfig()` converts to `DeploymentConfig`. Schema: ```yaml framework: hermes | openclaw provider: name: hetzner | digitalocean token: "..." ssh: names: [...] fingerprints: [...] server: name: "..." location: "ash" | "fsn1" | "nbg1" | "hel1" type: "cpx21" | ... inference: provider: venice | openrouter | openai | anthropic | custom api_key: "..." primary_model: "..." tailscale: enabled: true auth_key: "..." tailnet: "..." discord: enabled: true bot_token: "..." server_id: "..." ``` Full example: [`deploy.yaml.example`](deploy.yaml.example). --- ## Build System ### Makefile targets | Target | Description | |--------|-------------| | `make build` | Build binary for current platform | | `make test` | Run tests with race detection and coverage | | `make lint` | `go vet` + `gofmt` | | `make vet` | Run `go vet` | | `make fmt` | Format with `gofmt` | | `make clean` | Remove binary and coverage files | | `make cross-compile` | Build for linux-amd64, linux-arm64, darwin-arm64, windows-amd64, windows-arm64 | | `make version` | Print VERSION file contents | ### Version injection ```bash VERSION=$(cat VERSION) GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") LDFLAGS="-s -w -X main.version=$VERSION -X main.gitCommit=$GIT_COMMIT -X main.buildTime=$BUILD_TIME" go build $LDFLAGS -o obm ./cmd/obm ``` ### Docker Multi-stage Dockerfile: `golang:1.22-alpine` (build) → `alpine:3.20` (runtime). Non-root user `obm:1000`. Entry point: `obm --help`. --- ## CI/CD ### CI (`ci.yml`) Triggers on push/PR to main. Runs: `go vet` → `go test` → `gofmt` check → build. Build matrix: linux/darwin/windows × amd64/arm64 (excludes darwin/amd64). ### Release (`release.yml`) Triggers on tag push (`v*`). Builds cross-compiled binaries, creates archives (.tar.gz for Unix, .zip for Windows), generates SHA256 checksums, creates GitHub Release with upload. ### Release process ```bash ./scripts/release.sh v0.2.0 # This: validates version → runs tests → updates VERSION → commits → tags → pushes tag ``` Pre-release versions (containing hyphen, e.g. `v1.0.0-beta.1`) are marked as pre-release on GitHub. --- ## Cost Estimation `DeploymentConfig.MonthlyCostEstimate()` maps server types to price strings. Hetzner prices (current at time of writing): | Type | Price | |------|-------| | cx22 | €3.79/mo | | cx23 | €5.83/mo | | cpx21 | €4.49/mo | | cpx31 | €8.98/mo | | cpx41 | €17.96/mo | DigitalOcean prices: | Size | Price | |------|-------| | s-1vcpu-1gb | $6/mo | | s-1vcpu-2gb | $12/mo | | s-2vcpu-4gb | $24/mo | | s-4vcpu-8gb | $48/mo | | g-2vcpu-8gb | $63/mo | --- ## Terraform Integration `obm` generates the `.env` file that Terraform expects. The actual Terraform configs live in the separate [openboatmobile-ai](https://github.com/openboatmobile/openboatmobile-ai) repo. The `internal/terraform/terraform.go` wrapper provides: - `Runner.Init()` — `terraform init -input=false` - `Runner.Plan(destroy bool)` — `terraform plan` (with optional `-destroy` flag) - `Runner.Apply()` — `terraform apply -auto-approve` - `Runner.Destroy()` — `terraform destroy -auto-approve` All commands run in the `WorkDir` and capture combined output. ### Variable flow to cloud-init User sets `TF_VAR_*` env vars → sourced from `.env` → Terraform reads them → injected into cloud-init templates via `templatefile()` → written to server during provisioning. ### Cloud-init outputs **Hermes** (`userdata-hermes.tpl`): - `/home//.hermes/.env` — API keys, Discord token, gateway token - `/home//.hermes/config.yaml` — model config, Discord channels - `/home//.hermes/SOUL.md` — agent personality template - `/home//docker-compose.yml` — Docker mode only - `/etc/systemd/system/hermes.service` — systemd unit - `/usr/local/bin/hermes-health-check.sh` — diagnostic script **OpenClaw** (`userdata-openclaw.tpl`): - `/etc/openclaw.env` — secrets (0600, root-owned) - `/home//.openclaw/openclaw.json` — full config - `/etc/systemd/system/openclaw-gateway.service` - `/usr/local/bin/openclaw-health-check.sh` --- ## Destroy Flow `obm destroy` reads `terraform.tfstate` to list resources, shows them, asks for confirmation, runs `terraform destroy`, then cleans up state files unless `--keep-state` is set. --- ## Testing ```bash make test # Full suite with race detection + coverage go test ./... # Without make go test -v -race -coverprofile=coverage.out ./... ``` Test coverage includes: config parsing, dotenv round-tripping, YAML loading, inference client validation, Hetzner provider validation, destroy workflow, prompt helpers. --- ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide including development setup, PR checklist, and release process. Key points: - Go 1.22+ required - Run `make lint test` before pushing - Never push directly to `main` - Feature branches: `git checkout -b `