mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-31 00:40:46 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user