carlos / platform Public

host: the on-box state of a platform self-update

Request (agent-written hint), result, seen watermarks, applied, and
the journal, with durable writes: temp, fsync, rename, directory
fsync. Cache paths are derived from a validated version string.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QCUCpfxbCyEAVQ2m51dH5
Paul Campbell pushed by paul@keymail.dev 1e2f33ac85311164407d9a67b9bdf128aee58ba0 parent cab0d09
2 files changed, +315 −0
  • internal/host/systemstate.go +207 −0
  • internal/host/systemstate_test.go +108 −0
diff --git a/internal/host/systemstate.go b/internal/host/systemstate.go
new file mode 100644
index 000000000..5928e9504
--- /dev/null
+++ b/internal/host/systemstate.go
@@ -0,0 +1,207 @@
+package host
+
+import (
+ "encoding/json"
+ "errors"
+ "os"
+ "path/filepath"
+ "regexp"
+ "time"
+)
+
+// The on-box state of the platform self-update (spec §4, §6). Two homes:
+//
+// <data>/system/router/ — written by the sandboxed agent: the
+// request and the artifact cache. Root
+// reads exactly one file here and trusts
+// nothing in it beyond a version string.
+// <opt>/system/router/ — written by root: journal, seen (replay
+// watermarks), applied (the .prev
+// authority), result (what the agent
+// publishes). Root-owned, world-readable.
+const (
+ systemComponent = "router"
+ systemRequestMarker = "system-update-requested"
+ systemRequestEvery = time.Minute
+ systemArtifactLimit = int64(96 << 20)
+
+ systemOutcomeApplied = "applied"
+ systemOutcomeRolledBack = "rolled-back"
+ systemOutcomeRollbackFailed = "rollback-failed"
+ systemOutcomeRefused = "refused"
+ systemOutcomeDeferred = "deferred"
+ systemOutcomeNoted = "noted"
+
+ systemPhaseBackingUp = "backing-up"
+ systemPhaseInstalling = "installing"
+ systemPhaseInstalled = "installed"
+ systemPhaseUnits = "units"
+ systemPhaseRestarted = "restarted"
+ systemPhaseGated = "gated"
+)
+
+// systemRequest is the agent's hint to root: which promote it saw. No
+// channel (root's own host.env says), no key, no sha, no path (root derives
+// the cache path from the version and takes key and sha from the pointer).
+type systemRequest struct {
+ Version string `json:"version"`
+ PromotedAt time.Time `json:"promoted_at"`
+ NoOp bool `json:"noop"`
+ SeenAt time.Time `json:"seen_at"`
+}
+
+// systemResult is root's answer to the last request, keyed by the REQUEST's
+// tuple so a refusal of one promote never suppresses the next.
+type systemResult struct {
+ Channel string `json:"channel"`
+ Version string `json:"version"`
+ PromotedAt time.Time `json:"promoted_at"`
+ Outcome string `json:"outcome"`
+ Reason string `json:"reason,omitempty"`
+ At time.Time `json:"at"`
+}
+
+func (r *systemResult) matches(channel, version string, promotedAt time.Time) bool {
+ return r != nil && r.Channel == channel && r.Version == version && r.PromotedAt.Equal(promotedAt)
+}
+
+// terminal: outcomes the leg must not retry for the same pointer.
+func (r *systemResult) terminal() bool {
+ switch r.Outcome {
+ case systemOutcomeApplied, systemOutcomeRolledBack, systemOutcomeRollbackFailed, systemOutcomeRefused:
+ return true
+ }
+ return false
+}
+
+// systemApplied is the last build that PASSED A GATE on this box, forward or
+// rollback. It is the only thing that may authorise refreshing carlos.prev.
+type systemApplied struct {
+ Version string `json:"version"`
+ Channel string `json:"channel"`
+ At time.Time `json:"at"`
+}
+
+// systemSeen is the replay watermark per channel: the newest PromotedAt root
+// has verified. Advanced on every verified pointer, including no-ops.
+type systemSeen map[string]time.Time
+
+// systemJournal is the open transaction. Written before every box-changing
+// step, removed last; a run that finds one recovers before anything else.
+type systemJournal struct {
+ Phase string `json:"phase"`
+ Target string `json:"target"`
+ PrevBuild string `json:"prev_build"`
+ UnitsAdded []string `json:"units_added,omitempty"`
+ UnitsRemoved []string `json:"units_removed,omitempty"`
+ StartedAt time.Time `json:"started_at"`
+}
+
+var systemVersionRe = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
+
+// validSystemVersion is what makes the cache path derivable: no leading dot,
+// no separator, nothing that could be a traversal.
+func validSystemVersion(v string) bool { return systemVersionRe.MatchString(v) }
+
+func systemDataDir(dd string) string { return filepath.Join(dd, "system", systemComponent) }
+func systemRequestPath(dd string) string {
+ return filepath.Join(systemDataDir(dd), "requested.json")
+}
+func systemCachePath(dd, version string) string {
+ return filepath.Join(systemDataDir(dd), version, "carlos")
+}
+func systemRootDir(optDir string) string { return filepath.Join(optDir, "system", systemComponent) }
+func systemPrevPath(optDir string) string { return filepath.Join(optDir, "carlos.prev") }
+func systemBinaryPath(optDir string) string { return filepath.Join(optDir, "carlos") }
+
+// writeFileDurable: temp, fsync, rename, directory fsync. A crash leaves the
+// old file or the new one, never a torn one, and the rename itself is on
+// disk before the caller's next step depends on it.
+func writeFileDurable(path string, data []byte, mode os.FileMode) (err error) {
+ tmp := path + ".tmp"
+ f, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err != nil {
+ f.Close()
+ os.Remove(tmp)
+ }
+ }()
+ if _, err = f.Write(data); err != nil {
+ return err
+ }
+ if err = f.Chmod(mode); err != nil {
+ return err
+ }
+ if err = f.Sync(); err != nil {
+ return err
+ }
+ if err = f.Close(); err != nil {
+ return err
+ }
+ if err = os.Rename(tmp, path); err != nil {
+ return err
+ }
+ d, err := os.Open(filepath.Dir(path))
+ if err != nil {
+ return err
+ }
+ defer d.Close()
+ return d.Sync()
+}
+
+func writeJSONDurable(path string, v any, mode os.FileMode) error {
+ b, err := json.MarshalIndent(v, "", " ")
+ if err != nil {
+ return err
+ }
+ return writeFileDurable(path, append(b, '\n'), mode)
+}
+
+// readJSONFile leaves os.ErrNotExist intact so callers can treat "no file"
+// as a state, not a failure.
+func readJSONFile(path string, v any) error {
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(b, v)
+}
+
+func readSystemResult(optDir string) (*systemResult, error) {
+ var r systemResult
+ err := readJSONFile(filepath.Join(systemRootDir(optDir), "result.json"), &r)
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, nil
+ }
+ return &r, err
+}
+
+func readSystemSeen(optDir string) (systemSeen, error) {
+ s := systemSeen{}
+ err := readJSONFile(filepath.Join(systemRootDir(optDir), "seen.json"), &s)
+ if errors.Is(err, os.ErrNotExist) {
+ return s, nil
+ }
+ return s, err
+}
+
+func readSystemApplied(optDir string) (*systemApplied, error) {
+ var a systemApplied
+ err := readJSONFile(filepath.Join(systemRootDir(optDir), "applied.json"), &a)
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, nil
+ }
+ return &a, err
+}
+
+func readSystemJournal(optDir string) (*systemJournal, error) {
+ var j systemJournal
+ err := readJSONFile(filepath.Join(systemRootDir(optDir), "journal.json"), &j)
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, nil
+ }
+ return &j, err
+}
diff --git a/internal/host/systemstate_test.go b/internal/host/systemstate_test.go
new file mode 100644
index 000000000..69f0b68ca
--- /dev/null
+++ b/internal/host/systemstate_test.go
@@ -0,0 +1,108 @@
+package host
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestSystemPathsAreDerivedNotTrusted(t *testing.T) {
+ if got := systemCachePath("/data", "v1.2.3"); got != "/data/system/router/v1.2.3/carlos" {
+ t.Fatalf("cache path = %s", got)
+ }
+ if got := systemRequestPath("/data"); got != "/data/system/router/requested.json" {
+ t.Fatalf("request path = %s", got)
+ }
+ if got := systemRootDir("/opt/carlos"); got != "/opt/carlos/system/router" {
+ t.Fatalf("root dir = %s", got)
+ }
+ for _, bad := range []string{"", ".", "..", "../x", "a/b", ".hidden", "v1 2", string(make([]byte, 65))} {
+ if validSystemVersion(bad) {
+ t.Errorf("validSystemVersion(%q) = true", bad)
+ }
+ }
+ for _, good := range []string{"v0.18.0", "66587fa", "a", "release-2026.09.06_rc1"} {
+ if !validSystemVersion(good) {
+ t.Errorf("validSystemVersion(%q) = false", good)
+ }
+ }
+}
+
+func TestWriteFileDurableLeavesWholeFileOrNothing(t *testing.T) {
+ dir := t.TempDir()
+ p := filepath.Join(dir, "journal.json")
+ if err := writeFileDurable(p, []byte("one"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := writeFileDurable(p, []byte("two"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ b, _ := os.ReadFile(p)
+ if string(b) != "two" {
+ t.Fatalf("content = %q", b)
+ }
+ if _, err := os.Stat(p + ".tmp"); !errors.Is(err, os.ErrNotExist) {
+ t.Fatal("temp left behind")
+ }
+ st, _ := os.Stat(p)
+ if st.Mode().Perm() != 0o644 {
+ t.Fatalf("mode = %v", st.Mode().Perm())
+ }
+ // A directory that does not exist is an error, not a silent no-op.
+ if err := writeFileDurable(filepath.Join(dir, "no", "such", "f"), []byte("x"), 0o644); err == nil {
+ t.Fatal("wrote into a missing directory")
+ }
+}
+
+func TestSystemStateReadersRoundTrip(t *testing.T) {
+ opt := t.TempDir()
+ root := systemRootDir(opt)
+ if err := os.MkdirAll(root, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if r, err := readSystemResult(opt); err != nil || r != nil {
+ t.Fatalf("missing result: %v %v", r, err)
+ }
+ seen, err := readSystemSeen(opt)
+ if err != nil || len(seen) != 0 {
+ t.Fatalf("missing seen: %v %v", seen, err)
+ }
+ at := time.Date(2026, 9, 6, 9, 0, 0, 0, time.UTC)
+ res := systemResult{Channel: "stable", Version: "v1", PromotedAt: at, Outcome: systemOutcomeRolledBack, Reason: "gate", At: at}
+ if err := writeJSONDurable(filepath.Join(root, "result.json"), res, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ got, err := readSystemResult(opt)
+ if err != nil || !got.matches("stable", "v1", at) || !got.terminal() {
+ t.Fatalf("result = %+v err=%v", got, err)
+ }
+ if got.matches("canary", "v1", at) || got.matches("stable", "v2", at) || got.matches("stable", "v1", at.Add(time.Second)) {
+ t.Fatal("matches ignores part of the tuple")
+ }
+ got.Outcome = systemOutcomeDeferred
+ if got.terminal() {
+ t.Fatal("deferred is not terminal")
+ }
+ got.Outcome = systemOutcomeNoted
+ if got.terminal() {
+ t.Fatal("noted is not terminal for the leg: the version may still differ next time")
+ }
+ seen["stable"] = at
+ if err := writeJSONDurable(filepath.Join(root, "seen.json"), seen, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ seen2, _ := readSystemSeen(opt)
+ if !seen2["stable"].Equal(at) {
+ t.Fatalf("seen = %v", seen2)
+ }
+ j := systemJournal{Phase: systemPhaseInstalled, Target: "v1", PrevBuild: "v0", UnitsAdded: []string{"a.path"}, StartedAt: at}
+ if err := writeJSONDurable(filepath.Join(root, "journal.json"), j, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ j2, err := readSystemJournal(opt)
+ if err != nil || j2.Phase != systemPhaseInstalled || j2.UnitsAdded[0] != "a.path" {
+ t.Fatalf("journal = %+v err=%v", j2, err)
+ }
+}