15 KiB
DETAILS.md — Technical Reference
Companion to 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
VERSIONfile (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 <path> |
Interactive walkthrough (default) or non-interactive from YAML |
obm validate |
--env-file <path> |
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:
- Framework — Select Hermes or OpenClaw. Sets framework-specific defaults on
DeploymentConfig. - Cloud Provider — Hetzner or DigitalOcean.
- Provider Token — Enter API token. For Hetzner, validates against
/server_typesendpoint and lists SSH keys. - SSH Key — Select from keys found on the provider, or enter manually.
- Server Config — Name, location/region, server type/droplet size.
- Inference Provider — ZAI, Venice, OpenRouter. Enter API key. Validates against
/modelsendpoint. - Tailscale — Optional VPN setup. Auth key and tailnet domain.
- 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 = trueVeniceBaseURL = "https://api.venice.ai/api/v1"GatewayAllowAllUsers = trueDiscordAutoThread = true
OpenClaw:
OpenClawVersion = "lts"NodeVersion = "22"EnableSwap = true,SwapSizeGB = 2EnableFail2ban = 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 regionconfig.ServerTypeOrDroplet(cfg)— server type or droplet sizeconfig.SSHKeySummary(cfg)— masked SSH key display
DotEnvFile (internal/config/dotenv.go)
Round-trip parser for .env files:
ParseDotEnv(path)— parseTF_VAR_-prefixed env fileenv.GetVar(name)— lookup with and withoutTF_VAR_prefixenv.Values— rawmap[string]string
VarDef Schema (internal/config/schema.go)
Complete schema of all Terraform variables with metadata:
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/modelsendpoint, checks for HTTP 200- Returns
ValidationResult{Valid, ErrorMessage, ModelCount, Latency} - 30-second default timeout
Provider Interface (internal/provider/provider.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:
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_..._HEREplaceholders if empty WriteTfVars()generates HCL-formatterraform.tfvarsas 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:
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.
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
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
./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):
| 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 repo.
The internal/terraform/terraform.go wrapper provides:
Runner.Init()—terraform init -input=falseRunner.Plan(destroy bool)—terraform plan(with optional-destroyflag)Runner.Apply()—terraform apply -auto-approveRunner.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/<admin_user>/.hermes/.env— API keys, Discord token, gateway token/home/<admin_user>/.hermes/config.yaml— model config, Discord channels/home/<admin_user>/.hermes/SOUL.md— agent personality template/home/<admin_user>/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/<admin_user>/.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
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 for the full guide including development setup, PR checklist, and release process.
Key points:
- Go 1.22+ required
- Run
make lint testbefore pushing - Never push directly to
main - Feature branches:
git checkout -b <task-name>