diff --git a/cmd/ax/harness.go b/cmd/ax/harness.go index c028007b..1edf14db 100644 --- a/cmd/ax/harness.go +++ b/cmd/ax/harness.go @@ -70,6 +70,9 @@ func setHarnessWorkDir() error { if dir == "" { return nil } + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("create harness working directory %q: %w", dir, err) + } if err := os.Chdir(dir); err != nil { return fmt.Errorf("set harness working directory %q: %w", dir, err) } diff --git a/cmd/ax/harness_test.go b/cmd/ax/harness_test.go index 599f5abb..cabe160f 100644 --- a/cmd/ax/harness_test.go +++ b/cmd/ax/harness_test.go @@ -65,10 +65,14 @@ func TestSetHarnessWorkDir(t *testing.T) { } }) - t.Run("missing directory returns an error", func(t *testing.T) { - t.Setenv("AX_HARNESS_WORKDIR", filepath.Join(t.TempDir(), "does-not-exist")) - if err := setHarnessWorkDir(); err == nil { - t.Error("expected an error for a missing directory, got nil") + t.Run("missing directory is created", func(t *testing.T) { + dir := filepath.Join(t.TempDir(), "created", "workdir") + t.Setenv("AX_HARNESS_WORKDIR", dir) + if err := setHarnessWorkDir(); err != nil { + t.Fatalf("setHarnessWorkDir: %v", err) + } + if _, err := os.Stat(dir); err != nil { + t.Errorf("working directory %q was not created: %v", dir, err) } }) } diff --git a/internal/config/config.go b/internal/config/config.go index 58ec5008..ec89b8b1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -324,6 +324,9 @@ func (c *Config) Validate() error { } func AXAssetsDir() (string, error) { + if dir := os.Getenv("AX_DURABLE_DIR"); dir != "" { + return filepath.Join(dir, ".ax"), nil + } home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("resolving home directory: %w", err) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index baebb037..cdcda582 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -15,6 +15,8 @@ package config import ( + "os" + "path/filepath" "strings" "testing" @@ -345,3 +347,32 @@ func TestValidate_SkillsSelectionOneof(t *testing.T) { } }) } + +func TestAXAssetsDir(t *testing.T) { + t.Run("durable-dir env roots the .ax tree on the volume", func(t *testing.T) { + t.Setenv("AX_DURABLE_DIR", "/mnt/durable") + got, err := AXAssetsDir() + if err != nil { + t.Fatalf("AXAssetsDir: %v", err) + } + want := filepath.Join("/mnt/durable", ".ax") + if got != want { + t.Errorf("AXAssetsDir() = %q, want %q", got, want) + } + }) + t.Run("unset roots under the home directory", func(t *testing.T) { + t.Setenv("AX_DURABLE_DIR", "") + home, err := os.UserHomeDir() + if err != nil { + t.Skipf("no home directory: %v", err) + } + got, err := AXAssetsDir() + if err != nil { + t.Fatalf("AXAssetsDir: %v", err) + } + want := filepath.Join(home, ".ax") + if got != want { + t.Errorf("AXAssetsDir() = %q, want %q", got, want) + } + }) +} diff --git a/internal/harness/antigravityinteractions/antigravityinteractions.go b/internal/harness/antigravityinteractions/antigravityinteractions.go index cb84faf8..56c9f3fb 100644 --- a/internal/harness/antigravityinteractions/antigravityinteractions.go +++ b/internal/harness/antigravityinteractions/antigravityinteractions.go @@ -159,12 +159,12 @@ func cloudLocation() string { return defaultLocation } -// DefaultStateDir returns the default resume-cursor directory, ~/.ax/antigravityinteractions/cursors, -// used when a caller does not set StateDir explicitly. It lives outside the -// agent's working directory on purpose: the working directory is the agent's -// operating surface (it reads and edits files there), so AX's internal state is -// kept separate to avoid the agent seeing or clobbering it. New still requires a -// non-empty StateDir; callers apply this default. +// DefaultStateDir returns the default resume-cursor directory, +// /antigravityinteractions/cursors, used when a caller does not set +// StateDir explicitly. Locally that is ~/.ax/...; on a substrate actor +// AX_DURABLE_DIR points AXAssetsDir at a durable volume (e.g. /durable/.ax), kept +// outside the agent's working directory so the agent does not see or modify it. +// New still requires a non-empty StateDir; callers apply this default. func DefaultStateDir() (string, error) { axDir, err := config.AXAssetsDir() if err != nil { diff --git a/manifests/ax-deployment.yaml b/manifests/ax-deployment.yaml index fda42868..072682ff 100644 --- a/manifests/ax-deployment.yaml +++ b/manifests/ax-deployment.yaml @@ -97,7 +97,9 @@ spec: - name: GOOGLE_CLOUD_PROJECT value: "${GOOGLE_CLOUD_PROJECT}" - name: AX_HARNESS_WORKDIR - value: "/workspace" + value: "/durable/workspace" + - name: AX_DURABLE_DIR + value: "/durable" # Content of manifests/ax.yaml - name: AX_CONFIG_CONTENT value: "${AX_CONFIG_CONTENT}" @@ -105,8 +107,16 @@ spec: httpGet: path: /readyz port: 8081 + volumeMounts: + - name: durable + mountPath: /durable + volumes: + - name: durable + durableDir: {} snapshotsConfig: location: gs://${AX_SNAPSHOTS_BUCKET}/axinteractions/ + onPause: Data + onCommit: Data --- # ---------------------------------------------------------------------------