carlos / platform Public

settings-1 task 8: one pointer digest ratchet per process, armed by root and the agent

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ae5e811b891929dfa24975c4ee66e01a649a05dc parent 345aa02
5 files changed, +180 −2
  • internal/host/agent.go +6 −2
  • internal/host/pointerdigests.go +17 −0
  • internal/host/pointerdigests_test.go +100 −0
  • internal/release/ratchet_shared.go +27 −0
  • internal/release/ratchet_test.go +30 −0
diff --git a/internal/host/agent.go b/internal/host/agent.go
index c713732d2..680988c13 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -918,7 +918,8 @@ func materializeHook(reg *registry.Registry, dd string) func(context.Context, st
// The process's one proof gate (proofask.go), so a cold wake cannot
// adopt an unproven build of a listed app.
return release.MaterializeHost(ctx, reg, store, pub, optDir, dd, host,
- release.OnStageBlocked(stageReq), release.WithProofGate(agentProofGate()))
+ release.OnStageBlocked(stageReq), release.WithProofGate(agentProofGate()),
+ release.WithDigestRatchet(pointerRatchet(false, optDir, dd, os.Getenv)))
}
}
@@ -1000,7 +1001,8 @@ func adoptLoop(reg *registry.Registry, svc *activator.Service, sd *scheduleDeps,
release.RestartHibernating(restart),
release.WithStartUnit(makeStartRoute(reg, systemctlStartUnit)),
release.WithRollbackRestart(rollbackUnitRestart(reg, systemdUnits{}, optDir, os.Getenv)),
- release.WithProofGate(agentProofGate()), release.WithHostWrites(writes)); err != nil {
+ release.WithProofGate(agentProofGate()), release.WithHostWrites(writes),
+ release.WithDigestRatchet(pointerRatchet(false, optDir, dataDir(), os.Getenv))); err != nil {
log.Printf("agent: adopt pass: %v", err)
}
// The schedule converge's hourly floor (schedule spec §6.2), AFTER the
@@ -1673,6 +1675,7 @@ func fastPassOpts(dd string, stageReq func(host, ver string, err error), restart
release.OnStageBlocked(stageReq),
release.RestartHibernating(restart),
release.WithStartUnit(start),
+ release.WithDigestRatchet(pointerRatchet(false, release.OptDir(), dd, os.Getenv)),
}
}
@@ -4147,6 +4150,7 @@ func runAdoptWithEnsureSystemUnits(args []string, buildClaims ClaimFactory, ensu
// a relayed stage is exactly the request that performs such a restart.
passOpts := []release.PassOption{release.WithCapabilities(caps.Ops{}), release.BakeDir(dataDir()),
release.WithStartUnit(makeStartRoute(reg, systemctlStartUnit)), release.WithHostWrites(claims.Writes),
+ release.WithDigestRatchet(pointerRatchet(true, release.OptDir(), dataDir(), os.Getenv)),
release.WithRollbackRestart(rollbackUnitRestart(reg, systemdUnits{}, release.OptDir(), os.Getenv)),
// D3's gate: a fresh gate per root pass, so the policy file is read
// at the start of this pass (BeginPass) like every other. Its bake
diff --git a/internal/host/pointerdigests.go b/internal/host/pointerdigests.go
new file mode 100644
index 000000000..cafe317e5
--- /dev/null
+++ b/internal/host/pointerdigests.go
@@ -0,0 +1,17 @@
+package host
+
+import "github.com/carlosframework/platform/internal/release"
+
+// pointerRatchet is #253's ratchet for this process (spec §4.6), the one
+// SharedDigestRatchet keeps, so a fresh call per pass is the same object.
+// Root writes its marks under /opt (root-owned); the agent, sandboxed out of
+// /opt, writes under its data dir. Each reads both, so a mark either side
+// wrote protects the whole box. CARLOS_REQUIRE_POINTER_DIGEST applies to both.
+func pointerRatchet(root bool, optDir, dataDir string, getenv func(string) string) *release.DigestRatchet {
+ rootDir, edgeDir := release.RootRatchetDir(optDir), release.EdgeRatchetDir(dataDir)
+ require := release.RequirePointerDigest(getenv)
+ if root {
+ return release.SharedDigestRatchet(rootDir, []string{edgeDir}, require)
+ }
+ return release.SharedDigestRatchet(edgeDir, []string{rootDir}, require)
+}
diff --git a/internal/host/pointerdigests_test.go b/internal/host/pointerdigests_test.go
new file mode 100644
index 000000000..bfbcc562f
--- /dev/null
+++ b/internal/host/pointerdigests_test.go
@@ -0,0 +1,100 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func runtimePlatform() string { return runtime.GOOS + "-" + runtime.GOARCH }
+
+// Astra I2 #1: the agent cannot persist its mark; the next pass, built the
+// way production builds it, still refuses the digestless replay.
+func TestOneRatchetPerProcessSurvivesAFailedPersist(t *testing.T) {
+ if os.Geteuid() == 0 {
+ t.Skip("root can write a 0555 directory")
+ }
+ ctx := context.Background()
+ opt, data := t.TempDir(), t.TempDir()
+ if err := os.MkdirAll(release.EdgeRatchetDir(data), 0o555); err != nil {
+ t.Fatal(err)
+ }
+ store := release.NewDirStore(t.TempDir())
+ seed, pubB64, _ := release.GenerateKey()
+ priv, _ := release.ParseSigningKey(seed, "t")
+ pub, _ := release.ParsePublicKey(pubB64, "t")
+ ship := func(v string) {
+ m := release.Manifest{Schema: 1, App: "km", Version: v, Kind: "binary",
+ Artifacts: map[string]release.Artifact{runtimePlatform(): {}}}
+ if err := release.Ship(ctx, store, "bab", m, map[string][]byte{runtimePlatform(): []byte(v)}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ ship("v1")
+ if err := release.Promote(ctx, store, priv, "bab", "km", "canary/x", "v1"); err != nil { // legacy pointer
+ t.Fatal(err)
+ }
+ legacy, _, _ := store.Get(ctx, release.ChannelKey("bab", "km", "canary/x"))
+ if err := release.WriteDigestMarker(ctx, store, release.RootSigner(priv), "bab", "t"); err != nil {
+ t.Fatal(err)
+ }
+ ship("v2")
+ if err := release.Promote(ctx, store, priv, "bab", "km", "canary/x", "v2"); err != nil { // with digests
+ t.Fatal(err)
+ }
+ reg, err := registry.Open(filepath.Join(t.TempDir(), "r.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.Upsert(registry.Route{Host: "a.example.com", Kind: "instance", App: "km", Account: "bab", Channel: "canary/x"}); err != nil {
+ t.Fatal(err)
+ }
+ off := func(string) string { return "" }
+ pass := func() {
+ opts := []release.PassOption{release.WithDigestRatchet(pointerRatchet(false, opt, data, off))} // as fastPassOpts builds it, every pass
+ if err := release.PassFilter(ctx, reg, store, pub, opt, nil, nil, opts...); err != nil {
+ t.Fatal(err)
+ }
+ }
+ pass() // the digest pointer: its mark cannot be written, so it is refused
+ if v, _ := release.LiveVersion(opt, "a.example.com"); v != "" {
+ t.Fatalf("installed %q on a pointer whose mark was not durable", v)
+ }
+ _, etag, _ := store.Get(ctx, release.ChannelKey("bab", "km", "canary/x"))
+ if _, err := store.Put(ctx, release.ChannelKey("bab", "km", "canary/x"), legacy, etag); err != nil {
+ t.Fatal(err)
+ }
+ pass() // the replay, on a new pass
+ if v, _ := release.LiveVersion(opt, "a.example.com"); v != "" {
+ t.Fatalf("a later pass in the same process installed the digestless replay %q", v)
+ }
+}
+
+func TestPointerRatchetSharesMarksBetweenRootAndTheAgent(t *testing.T) {
+ opt, data := t.TempDir(), t.TempDir()
+ off := func(string) string { return "" }
+ p := &release.ChannelPointer{App: "km", Channel: "stable", Version: "v1", Digest: release.ManifestDigest([]byte("m"))}
+ if err := pointerRatchet(true, opt, data, off).Admit("bab", p); err != nil {
+ t.Fatal(err)
+ }
+ legacy := &release.ChannelPointer{App: "km", Channel: "stable", Version: "v1"}
+ if err := pointerRatchet(false, opt, data, off).Admit("bab", legacy); !errors.Is(err, release.ErrPointerWithoutDigests) {
+ t.Fatalf("the agent ignored root's mark: %v", err)
+ }
+ on := func(k string) string {
+ if k == release.RequirePointerDigestEnv {
+ return "on"
+ }
+ return ""
+ }
+ fresh := &release.ChannelPointer{App: "km", Channel: "beta", Version: "v1"}
+ if err := pointerRatchet(false, opt, data, on).Admit("bab", fresh); !errors.Is(err, release.ErrPointerWithoutDigests) {
+ t.Fatalf("the flag was not read: %v", err)
+ }
+}
diff --git a/internal/release/ratchet_shared.go b/internal/release/ratchet_shared.go
new file mode 100644
index 000000000..d3725027b
--- /dev/null
+++ b/internal/release/ratchet_shared.go
@@ -0,0 +1,27 @@
+package release
+
+import "sync"
+
+var (
+ sharedMu sync.Mutex
+ sharedRatchets = map[string]*DigestRatchet{}
+)
+
+// SharedDigestRatchet is the process's one ratchet for marks written under
+// own (Astra I2 #1): every pass, materialization, held stage and static read
+// in this process gets the same object, so a channel remembered after a mark
+// could not be persisted stays remembered. require, once on, stays on.
+func SharedDigestRatchet(own string, also []string, require bool) *DigestRatchet {
+ sharedMu.Lock()
+ defer sharedMu.Unlock()
+ r, ok := sharedRatchets[own]
+ if !ok {
+ r = NewDigestRatchet(own, also, require)
+ sharedRatchets[own] = r
+ return r
+ }
+ if require {
+ r.require.Store(true) // atomic: Admit may be reading it right now (Astra I3 #7)
+ }
+ return r
+}
diff --git a/internal/release/ratchet_test.go b/internal/release/ratchet_test.go
index 4bd0fc965..5f0ef6926 100644
--- a/internal/release/ratchet_test.go
+++ b/internal/release/ratchet_test.go
@@ -4,6 +4,7 @@ import (
"errors"
"os"
"path/filepath"
+ "sync"
"testing"
)
@@ -121,3 +122,32 @@ func TestRatchetDirs(t *testing.T) {
t.Fatal("ratchet dirs moved")
}
}
+
+func TestSharedRatchetIsOnePerDirectory(t *testing.T) {
+ d := t.TempDir()
+ if SharedDigestRatchet(d, nil, false) != SharedDigestRatchet(d, nil, false) {
+ t.Fatal("two calls for the same directory built two ratchets")
+ }
+}
+
+// Astra I3 #7: lookups that widen require race with Admit's read on the one
+// shared object. Run under -race; with a plain bool field the detector fails it.
+func TestSharedRatchetLookupAndAdmitAreRaceFree(t *testing.T) {
+ d := t.TempDir()
+ legacy := &ChannelPointer{App: "km", Channel: "stable", Version: "v1"}
+ var wg sync.WaitGroup
+ for i := 0; i < 16; i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ for j := 0; j < 200; j++ {
+ r := SharedDigestRatchet(d, nil, i%2 == 0)
+ _ = r.Admit("bab", legacy)
+ }
+ }(i)
+ }
+ wg.Wait()
+ if err := SharedDigestRatchet(d, nil, false).Admit("bab", legacy); !errors.Is(err, ErrPointerWithoutDigests) {
+ t.Fatalf("require, widened by a lookup, was lost: %v", err)
+ }
+}