diff --git a/cmd/ax/harness.go b/cmd/ax/harness.go index 1985696a..e5660bd6 100644 --- a/cmd/ax/harness.go +++ b/cmd/ax/harness.go @@ -19,6 +19,7 @@ package main import ( "context" + "encoding/base64" "fmt" "log" "net" @@ -142,12 +143,34 @@ func runAntigravityInteractionsHarness(ctx context.Context) error { if err := setHarnessWorkDir(); err != nil { return err } - stateDir, err := antigravityinteractions.DefaultStateDir() - if err != nil { - return err + + // Read harness config from ax.yaml handed to the actor via AX_CONFIG_CONTENT. + // Fall back to built-in defaults. + var hc config.AntigravityInteractionsHarnessConfig + if raw := os.Getenv("AX_CONFIG_CONTENT"); raw != "" { + if data, err := base64.StdEncoding.DecodeString(raw); err != nil { + log.Printf("AX_CONFIG_CONTENT: base64 decode failed, using defaults: %v", err) + } else if cfg, err := config.LoadFromBytes(data); err != nil { + log.Printf("AX_CONFIG_CONTENT: parse failed, using defaults: %v", err) + } else { + hc = cfg.Harnesses.AntigravityInteractions + } } + agent := hc.Agent + if agent == "" { + agent = antigravityinteractions.DefaultAgent + } + stateDir := hc.StateDir + if stateDir == "" { + var err error + stateDir, err = antigravityinteractions.DefaultStateDir() + if err != nil { + return err + } + } + cfg := antigravityinteractions.AntigravityInteractionsConfig{ - Agent: antigravityinteractions.DefaultAgent, + Agent: agent, StateDir: stateDir, } return antigravityinteractions.Serve(ctx, cfg, harnessHost, harnessPort, harnessReadyzPort) diff --git a/hack/install-ax.sh b/hack/install-ax.sh index f4a2025b..249f30e2 100755 --- a/hack/install-ax.sh +++ b/hack/install-ax.sh @@ -230,6 +230,12 @@ deploy_ax_server() { echo "Using existing Postgres via AX_EVENTLOG_DSN." >&2 fi + # Base64 of the single-source config file (manifests/ax.yaml), injected into + # each ActorTemplate's AX_CONFIG_CONTENT env so substrate actors read their config + # without a mounted ConfigMap. + local ax_config_content + ax_config_content="$(base64 < manifests/ax.yaml | tr -d '\n')" + # Common substitutions applied to every rendered manifest. local render_sed=( -e "s|\${GEMINI_API_KEY}|${GEMINI_API_KEY}|g" @@ -237,9 +243,10 @@ deploy_ax_server() { -e "s|\${AX_IMAGE}|${ax_image}|g" -e "s|\${ATEOM_IMAGE}|${ateom_image}|g" -e "s|\${GOOGLE_CLOUD_PROJECT}|${GOOGLE_CLOUD_PROJECT:-}|g" + -e "s|\${AX_CONFIG_CONTENT}|${ax_config_content}|g" ) - # Render and apply the core manifest (namespace, harnesses, ax-server, ConfigMap). + # Render and apply the core manifest (namespace, harnesses, ax-server). if ! sed "${render_sed[@]}" manifests/ax-deployment.yaml | run_kubectl apply -f -; then echo >&2 echo "Error: cluster rejected the manifest. An \"unknown field\" error usually means the" >&2 @@ -248,6 +255,13 @@ deploy_ax_server() { exit 1 fi + # Create/update the controller ConfigMap from the single-source config file + # (manifests/ax.yaml) -- the same file that seeds the actors' AX_CONFIG_CONTENT. + # ax-server mounts it at /etc/ax/ax.yaml. + run_kubectl -n ax create configmap ax-server-config \ + --from-file=ax.yaml=manifests/ax.yaml \ + --dry-run=client -o yaml | run_kubectl apply -f - + # Create/update the event-log Secret with the DSN (and, for the bundled Postgres, # its password). ax-server reads AX_EVENTLOG_DSN from this Secret's dsn key. local secret_args=(--from-literal=dsn="${pg_dsn}") diff --git a/internal/config/config.go b/internal/config/config.go index 7190d6bf..d4bb10f4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -144,10 +144,14 @@ func LoadFromFile(path string) (*Config, error) { if err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } + return LoadFromBytes(data) +} +// LoadFromBytes parses configuration from YAML bytes and applies defaults. +func LoadFromBytes(data []byte) (*Config, error) { var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("failed to parse config file: %w", err) + return nil, fmt.Errorf("failed to parse config: %w", err) } cfg.setDefaults() diff --git a/internal/config/config_test.go b/internal/config/config_test.go index dec9db3b..e137d049 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -156,3 +156,32 @@ harnesses: t.Errorf("StateDir = %q, want %q", got, want) } } + +func TestLoadFromBytes(t *testing.T) { + cfg, err := LoadFromBytes([]byte(` +version: v1alpha +harnesses: + antigravity: + default: true +`)) + if err != nil { + t.Fatalf("LoadFromBytes: %v", err) + } + if cfg.Version != "v1alpha" { + t.Errorf("Version = %q, want %q", cfg.Version, "v1alpha") + } + if !cfg.Harnesses.Antigravity.Default { + t.Error("Harnesses.Antigravity.Default = false, want true") + } + // setDefaults must run (same as LoadFromFile). + if got, want := cfg.Server.Address, ":8494"; got != want { + t.Errorf("Server.Address = %q, want default %q", got, want) + } +} + +// TestLoadFromBytes_Invalid returns an error on malformed YAML. +func TestLoadFromBytes_Invalid(t *testing.T) { + if _, err := LoadFromBytes([]byte("harnesses: [unterminated")); err == nil { + t.Fatal("LoadFromBytes(invalid): got nil error, want error") + } +} diff --git a/manifests/ax-deployment.yaml b/manifests/ax-deployment.yaml index ff95ad03..1c45dace 100644 --- a/manifests/ax-deployment.yaml +++ b/manifests/ax-deployment.yaml @@ -98,6 +98,9 @@ spec: value: "${GOOGLE_CLOUD_PROJECT}" - name: AX_HARNESS_WORKDIR value: "/workspace" + # Content of manifests/ax.yaml + - name: AX_CONFIG_CONTENT + value: "${AX_CONFIG_CONTENT}" readyz: httpGet: path: /readyz @@ -149,19 +152,3 @@ spec: - name: ax-config configMap: name: ax-server-config ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: ax-server-config - namespace: ax -data: - ax.yaml: | - version: v1alpha - eventlog: - postgres: - dsn: ${AX_EVENTLOG_DSN} - harnesses: - antigravity: - default: true - antigravity-interactions: {} diff --git a/manifests/ax.yaml b/manifests/ax.yaml new file mode 100644 index 00000000..8c243fef --- /dev/null +++ b/manifests/ax.yaml @@ -0,0 +1,30 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Deploy configuration for AX on Agent Substrate. install-ax.sh renders this +# single source into two places: +# - the ax-server-config ConfigMap (mounted at /etc/ax/ax.yaml), read by the +# controller (`ax serve`); and +# - the AX_CONFIG_CONTENT env (base64 of this file) on each ActorTemplate, +# so a substrate actor can read its config. +version: v1alpha + +eventlog: + postgres: + dsn: ${AX_EVENTLOG_DSN} + +harnesses: + antigravity: + default: true + antigravity-interactions: {}