From d2cbe50e3dab06a7f2d5ef09f46df29e3711787c Mon Sep 17 00:00:00 2001 From: J Date: Tue, 14 Jul 2026 20:03:31 +0800 Subject: [PATCH] fix(daemon): fixed-length Codex store namespace so long profiles fit (MUL-4424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Elon's eighth-review blocker: hex-encoding the full profile doubled the namespace segment length. A profile can be as long as a filesystem segment allows (~255 bytes) and the CLI persists it as its own config dir, but the store namespace "p_" + hex(profile) reached 2 + 127*2 = 256 bytes at 127 chars, overflowing the 255-byte single-segment limit — the profile's own dir created fine, then the session store failed with "file name too long". Derive the namespace from a fixed-length hash instead: a named profile is now "p_" + hex(sha256(profile)) — a constant 64 hex chars (66 with the prefix), filesystem-safe and collision-resistant. The empty (default) profile keeps its reserved bare literal "default", which the "p_"-prefixed 66-char segment can never equal. Still injective across the CLI's distinct-daemon cases; just no longer length-expanding. Test: the namespace stays <=255 bytes and creatable for profiles up to the 255-byte segment limit (127- and 255-char cases that overflowed under hex); the prior injectivity and cross-profile prune-isolation tests still hold. execenv + daemon under -race, go vet, gofmt all pass. Co-authored-by: multica-agent --- server/internal/daemon/execenv/codex_home.go | 21 ++++++++++++------- .../execenv/codex_home_sessions_test.go | 20 ++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/server/internal/daemon/execenv/codex_home.go b/server/internal/daemon/execenv/codex_home.go index f8e40611ec..a2867fb8c6 100644 --- a/server/internal/daemon/execenv/codex_home.go +++ b/server/internal/daemon/execenv/codex_home.go @@ -1,6 +1,7 @@ package execenv import ( + "crypto/sha256" "encoding/hex" "fmt" "io" @@ -218,18 +219,22 @@ func codexSessionStoreDir(sharedHome, key string) string { // reclaims, its own namespace, so a staging daemon's GC can never delete a // production task's live store and vice versa. // -// The map MUST be injective: distinct profiles are distinct daemons and must -// never share a namespace. A lossy "drop unsafe characters" scheme is not — the -// CLI treats "" and "default" as separate daemons, and would sanitize -// "staging.prod" and "stagingprod" to the same segment. So the empty (default) -// profile gets a reserved bare literal, and every named profile is hex-encoded -// (bijective, filesystem-safe) under a "p_" prefix a bare literal can never -// collide with (MUL-4424). +// The map MUST be collision-free (distinct profiles are distinct daemons and +// must never share a namespace) AND fixed-length (a profile can be as long as a +// filesystem segment allows, ~255 bytes, so any length-expanding encoding would +// overflow the 255-byte limit and fail to create the store dir). A lossy "drop +// unsafe characters" scheme collides ("" vs "default", "staging.prod" vs +// "stagingprod"); a full hex encoding doubles the length and overflows. So the +// empty (default) profile gets a reserved bare literal, and every named profile +// is the hex of its SHA-256 — a constant 64 hex chars, filesystem-safe and +// collision-resistant — under a "p_" prefix the bare literal can never collide +// with (MUL-4424). func codexSessionStoreNamespace(profile string) string { if profile == "" { return "default" } - return "p_" + hex.EncodeToString([]byte(profile)) + sum := sha256.Sum256([]byte(profile)) + return "p_" + hex.EncodeToString(sum[:]) } // codexSessionStoreKey builds the per-(profile, agent, issue) key for a task's diff --git a/server/internal/daemon/execenv/codex_home_sessions_test.go b/server/internal/daemon/execenv/codex_home_sessions_test.go index 2bdf196349..2e482817a8 100644 --- a/server/internal/daemon/execenv/codex_home_sessions_test.go +++ b/server/internal/daemon/execenv/codex_home_sessions_test.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "testing" "time" ) @@ -620,6 +621,25 @@ func TestCodexSessionStoreNamespaceInjective(t *testing.T) { } } +// TestCodexSessionStoreNamespace_FitsDirectorySegment guards Elon's round-8 +// blocker: a profile the CLI can persist as its own config-dir segment (up to +// the ~255-byte filesystem limit) must yield a namespace that also fits one +// segment and is creatable — a length-expanding encoding (full hex) overflowed +// at 127 bytes. MUL-4424. +func TestCodexSessionStoreNamespace_FitsDirectorySegment(t *testing.T) { + t.Parallel() + base := t.TempDir() + for _, profile := range []string{"", "staging", strings.Repeat("a", 127), strings.Repeat("z", 255)} { + ns := codexSessionStoreNamespace(profile) + if len(ns) > 255 { + t.Errorf("profile (len %d) -> namespace (len %d) exceeds the 255-byte single-segment limit", len(profile), len(ns)) + } + if err := os.MkdirAll(filepath.Join(base, ns), 0o755); err != nil { + t.Errorf("namespace for profile (len %d) could not be created: %v", len(profile), err) + } + } +} + // TestPruneCodexSessionStores_NoCrossProfileCollision proves the injective // namespace holds end-to-end: profiles the CLI treats as distinct never reclaim // each other's stores, including the "" vs "default" and punctuation cases that