fix(daemon): fixed-length Codex store namespace so long profiles fit (MUL-4424)

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 <github@multica.ai>
This commit is contained in:
J
2026-07-14 20:03:31 +08:00
parent b6d30b6e89
commit d2cbe50e3d
2 changed files with 33 additions and 8 deletions

View File

@@ -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

View File

@@ -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