carlos / platform Public

settings-1 task 6: the per-box pointer digest ratchet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev af49b4065d61fb2c05dbffafa5d6fd79d5655311 parent 4c531a4
2 files changed, +261 −0
  • internal/release/ratchet.go +138 −0
  • internal/release/ratchet_test.go +123 −0
diff --git a/internal/release/ratchet.go b/internal/release/ratchet.go
new file mode 100644
index 000000000..74d2d4d1a
--- /dev/null
+++ b/internal/release/ratchet.go
@@ -0,0 +1,138 @@
+package release
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+)
+
+// RequirePointerDigestEnv is the box flag that refuses every channel pointer
+// without digests, even for a channel this box has never seen (spec §4.6, the
+// fleet-wide switch). Off by default; it goes on once `carlos channels
+// digests` lists nothing.
+const RequirePointerDigestEnv = "CARLOS_REQUIRE_POINTER_DIGEST"
+
+// ErrPointerWithoutDigests is a pointer the ratchet or the flag refuses.
+var ErrPointerWithoutDigests = errors.New("release: pointer carries no digests")
+
+// ErrRatchetUnpersisted is a pointer with digests whose channel mark could
+// not be written: nothing installs or extracts on it until the mark is
+// durable, because a restart would otherwise forget the channel (Astra I1 #4).
+var ErrRatchetUnpersisted = errors.New("release: the pointer digest mark could not be persisted")
+
+// RequirePointerDigest reads the flag: 1, true, on or yes in any case.
+func RequirePointerDigest(getenv func(string) string) bool {
+ switch strings.ToLower(strings.TrimSpace(getenv(RequirePointerDigestEnv))) {
+ case "1", "true", "on", "yes":
+ return true
+ }
+ return false
+}
+
+// RootRatchetDir is root's marks (/opt/carlos/pointer-digests), and
+// EdgeRatchetDir the sandboxed agent's and edge's, which cannot write /opt
+// (/data/pointer-digests). Each process writes its own and reads both.
+func RootRatchetDir(optDir string) string { return filepath.Join(optDir, "pointer-digests") }
+func EdgeRatchetDir(dataDir string) string { return filepath.Join(dataDir, "pointer-digests") }
+
+// DigestRatchet is the per-box read-side protection of #253 (spec §4.6):
+// once this box has verified a pointer with digests for a channel, it
+// refuses pointers without them for that channel, so a saved old pointer
+// cannot be replayed to it. Marks are files, so they outlive the process;
+// they are never cleared, and removing the emission marker does not clear
+// them.
+type DigestRatchet struct {
+ own string
+ also []string
+ // require is atomic: SharedDigestRatchet (Task 8) may widen it while
+ // another goroutine's Admit reads it (Astra I3 #7).
+ require atomic.Bool
+
+ mu sync.Mutex
+ seen map[string]bool // channels found marked, in any dir, by this process
+}
+
+// NewDigestRatchet reads marks from own and also, and writes them to own.
+func NewDigestRatchet(own string, also []string, require bool) *DigestRatchet {
+ r := &DigestRatchet{own: own, also: also, seen: map[string]bool{}}
+ r.require.Store(require)
+ return r
+}
+
+// markPath is a channel's mark. The ".digests" suffix lets "canary" and
+// "canary/x" both be marked: the second needs a directory named canary.
+func markPath(dir, acct, app, channel string) string {
+ return filepath.Join(dir, acct, app, filepath.FromSlash(channel)+".digests")
+}
+
+// Admit is the ratchet's verdict on a verified pointer. A nil ratchet admits
+// everything, which is today's behaviour for any caller that does not arm
+// one. A pointer with digests marks its channel: in memory at once, so this
+// process refuses a digestless replay from that moment, and on disk before
+// the pointer is admitted. A mark that cannot be persisted refuses the
+// pointer (ErrRatchetUnpersisted): installing or extracting on it would leave
+// the box unprotected after a restart.
+func (r *DigestRatchet) Admit(acct string, p *ChannelPointer) error {
+ if r == nil || p == nil {
+ return nil
+ }
+ if p.CarriesDigests() {
+ return r.note(acct, p.App, p.Channel)
+ }
+ if r.require.Load() {
+ return fmt.Errorf("%w: %s/%s/%s, and %s is on", ErrPointerWithoutDigests, acct, p.App, p.Channel, RequirePointerDigestEnv)
+ }
+ if r.marked(acct, p.App, p.Channel) {
+ return fmt.Errorf("%w: %s/%s/%s, and this box has already verified one with digests for that channel",
+ ErrPointerWithoutDigests, acct, p.App, p.Channel)
+ }
+ return nil
+}
+
+func (r *DigestRatchet) marked(acct, app, channel string) bool {
+ k := acct + "|" + app + "|" + channel
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ if r.seen[k] {
+ return true
+ }
+ for _, dir := range append([]string{r.own}, r.also...) {
+ if dir == "" {
+ continue
+ }
+ if _, err := os.Stat(markPath(dir, acct, app, channel)); err == nil {
+ r.seen[k] = true
+ return true
+ }
+ }
+ return false
+}
+
+func (r *DigestRatchet) note(acct, app, channel string) error {
+ k := acct + "|" + app + "|" + channel
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.seen[k] = true // remembered at once, whatever the disk says
+ if r.own == "" {
+ return nil
+ }
+ path := markPath(r.own, acct, app, channel)
+ if _, err := os.Stat(path); err == nil {
+ return nil
+ }
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ log.Printf("release: pointer digest ratchet: %v", err)
+ return fmt.Errorf("%w: %v", ErrRatchetUnpersisted, err)
+ }
+ if err := WriteFileAtomic(path, []byte(time.Now().UTC().Format(time.RFC3339)+"\n"), 0o644); err != nil {
+ log.Printf("release: pointer digest ratchet: %v", err)
+ return fmt.Errorf("%w: %v", ErrRatchetUnpersisted, err)
+ }
+ return nil
+}
diff --git a/internal/release/ratchet_test.go b/internal/release/ratchet_test.go
new file mode 100644
index 000000000..4bd0fc965
--- /dev/null
+++ b/internal/release/ratchet_test.go
@@ -0,0 +1,123 @@
+package release
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func ptr(ch string, digest string) *ChannelPointer {
+ return &ChannelPointer{App: "km", Channel: ch, Version: "v1", Digest: digest}
+}
+
+func TestRatchetArmsPerChannel(t *testing.T) {
+ r := NewDigestRatchet(t.TempDir(), nil, false)
+ if err := r.Admit("bab", ptr("stable", "")); err != nil {
+ t.Fatalf("a fresh box refused a legacy pointer: %v", err)
+ }
+ if err := r.Admit("bab", ptr("stable", dA)); err != nil {
+ t.Fatal(err)
+ }
+ if err := r.Admit("bab", ptr("stable", "")); !errors.Is(err, ErrPointerWithoutDigests) {
+ t.Fatalf("after digests: %v, want ErrPointerWithoutDigests", err)
+ }
+ if err := r.Admit("bab", ptr("beta", "")); err != nil {
+ t.Fatalf("another channel is not ratcheted: %v", err)
+ }
+ if err := r.Admit("cdf", ptr("stable", "")); err != nil {
+ t.Fatalf("another account is not ratcheted: %v", err)
+ }
+}
+
+func TestRatchetSurvivesARestart(t *testing.T) {
+ dir := t.TempDir()
+ if err := NewDigestRatchet(dir, nil, false).Admit("bab", ptr("stable", dA)); err != nil {
+ t.Fatal(err)
+ }
+ if err := NewDigestRatchet(dir, nil, false).Admit("bab", ptr("stable", "")); !errors.Is(err, ErrPointerWithoutDigests) {
+ t.Fatalf("a new process forgot the mark: %v", err)
+ }
+}
+
+func TestRequireRefusesEveryPointerWithoutDigests(t *testing.T) {
+ r := NewDigestRatchet(t.TempDir(), nil, true)
+ if err := r.Admit("bab", ptr("never-seen", "")); !errors.Is(err, ErrPointerWithoutDigests) {
+ t.Fatalf("require on, fresh channel: %v", err)
+ }
+ if err := r.Admit("bab", ptr("never-seen", dA)); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRequirePointerDigestReadsTheFlag(t *testing.T) {
+ for v, want := range map[string]bool{"": false, "0": false, "off": false, "1": true, "true": true, "ON": true, "yes": true} {
+ got := RequirePointerDigest(func(k string) string {
+ if k == RequirePointerDigestEnv {
+ return v
+ }
+ return ""
+ })
+ if got != want {
+ t.Errorf("%q: %v, want %v", v, got, want)
+ }
+ }
+}
+
+// Review Focus 3.
+func TestRatchetMarksNestedAndFlatChannels(t *testing.T) {
+ r := NewDigestRatchet(t.TempDir(), nil, false)
+ if err := r.Admit("bab", ptr("canary", dA)); err != nil {
+ t.Fatal(err)
+ }
+ if err := r.Admit("bab", ptr("canary/x", "")); err != nil {
+ t.Fatalf("marking canary refused canary/x: %v", err)
+ }
+ if err := r.Admit("bab", ptr("canary/x", dA)); err != nil {
+ t.Fatalf("canary/x could not be marked beside canary: %v", err)
+ }
+ if err := r.Admit("bab", ptr("canary/x", "")); !errors.Is(err, ErrPointerWithoutDigests) {
+ t.Fatalf("canary/x not ratcheted: %v", err)
+ }
+}
+
+// Review Focus 4, Astra I1 #4: a mark that cannot be persisted fails closed.
+// The pointer that carried digests is refused (nothing installs or extracts
+// on it), the process remembers the channel at once, and a mark another
+// process wrote is still honoured.
+func TestRatchetFailsClosedWhenTheMarkCannotBePersisted(t *testing.T) {
+ ro := filepath.Join(t.TempDir(), "ro")
+ if err := os.MkdirAll(ro, 0o555); err != nil {
+ t.Fatal(err)
+ }
+ if os.Geteuid() == 0 {
+ t.Skip("root can write a 0555 directory")
+ }
+ other := t.TempDir()
+ if err := NewDigestRatchet(other, nil, false).Admit("bab", ptr("stable", dA)); err != nil {
+ t.Fatal(err)
+ }
+ r := NewDigestRatchet(ro, []string{other}, false)
+ if err := r.Admit("bab", ptr("beta", dA)); !errors.Is(err, ErrRatchetUnpersisted) {
+ t.Fatalf("an unpersisted mark admitted the pointer: %v", err)
+ }
+ if err := r.Admit("bab", ptr("beta", "")); !errors.Is(err, ErrPointerWithoutDigests) {
+ t.Fatalf("the same process forgot the channel it just saw with digests: %v", err)
+ }
+ if err := r.Admit("bab", ptr("stable", "")); !errors.Is(err, ErrPointerWithoutDigests) {
+ t.Fatalf("the other process's mark was not honoured: %v", err)
+ }
+}
+
+func TestANilRatchetAdmitsEverything(t *testing.T) {
+ var r *DigestRatchet
+ if err := r.Admit("bab", ptr("stable", "")); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRatchetDirs(t *testing.T) {
+ if RootRatchetDir("/opt/carlos") != "/opt/carlos/pointer-digests" || EdgeRatchetDir("/data") != "/data/pointer-digests" {
+ t.Fatal("ratchet dirs moved")
+ }
+}