- Status: done
- Date: 2026-06-19
- Specs touched:
ENVIRONMENT.md(governing spec — implemented, not changed)
First implementation slice of the cloud-VM track in #196 (the C1 profile foundation, split marker → image profile → slim host-agent). The hosted environment profile was specced in #200 (ENVIRONMENT.md, pass 1); the most recent #196 progress entry is the mkosi-no-ISO finding. This is C1a (#202): the runtime-marker contract plus the brain-side reader. It is foundational and mkosi-independent — no image, host-agent, or behavioral-seam change. No seam consults the profile yet; each hosted behavior (skip mDNS publish, etc.) branches on it when its feature lands.
internal/profile/(new) — a tiny provider package, concrete types only (no interface; consumer-side seams take the value, perCLAUDE.md# Go code discipline). ExportsProfile(Appliance|Hosted),DefaultMarkerPath(/etc/malmo/profile), andRead(path) Profile.Readnever fails: an absent marker (the unmarked appliance box andmake dev), an unreadable one, or unrecognized content all resolve toAppliance— the no-op default, so an unmarked box behaves exactly as today (ENVIRONMENT.md# How the profile is realized, "A runtime marker"). Surrounding whitespace / a trailing newline are tolerated (strings.TrimSpace). Logging is asymmetric on purpose: an absent marker is the expected appliance case and is silent, while a present-but-unreadable marker and a present-but-unrecognized value are bothslog.Warn-ed — they signal a mis-stamped or mis-permissioned image, and silently defaulting could hide a hosted box running in the wrong posture.cmd/brain/main.go— newprofilePathconfig (envMALMO_PROFILE_PATH, defaultprofile.DefaultMarkerPath) so tests andmake devcan point it elsewhere. The brain callsprofile.Readonce at startup (right after the logger is installed) into a localprofand logs the resolved value viaslog.Info("environment profile resolved", "profile", …).profis held inmainfor future seams to consult; nothing threads it into a constructor yet, by design — that happens alongside the first consuming seam (don't build a DI container or thread a logger to carry it).CLAUDE.md# Go code discipline — addedprofileto the standard structured-fields list (with a one-line definition) so journalctl/jq filters on the new field stay stable.- Tests —
internal/profile/profile_test.gocovers the value matrix: absent → appliance (no warn),hosted→ hosted,appliance→ appliance, trailing-whitespace/newline tolerated, empty / unknown / mis-cased → appliance + warn, and a present-but-unreadable marker (a directory, deterministic under any uid) → appliance + warn. 100% statement coverage of the package;make checkgreen.
- C1b (#203) — the lean hosted
mkosiimage profile that actually stamps/etc/malmo/profilewithhosted(and installs only what a cloud VM needs). Until it lands, no real image carries the marker, so every box reads asappliance. - C1c (#204) — the build-tagged slim cloud
host-agent. host-agent's profile behavior is determined by its build tag, not this marker (ENVIRONMENT.md# How the profile is realized). - First behavioral seam — the marker is read and logged but not yet consumed. The first hosted seam to wire (a candidate: skip the Avahi/mDNS publish in
hosted) threadsproffrommaininto that one call site, consumer-side.