Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ proto:
test:
@echo "Running tests..."
@go test -v ./...
@echo "Running tests (harness path)..."
@go test -v -tags harness ./...

# Clean build artifacts
clean:
Expand Down
3 changes: 1 addition & 2 deletions cmd/ax/fork.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"fmt"

"github.com/google/ax/cmd/ax/internal/cliutil"
"github.com/google/ax/internal/config"
"github.com/google/ax/proto"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -54,7 +53,7 @@ func runFork(cmd *cobra.Command, args []string) error {

if forkServerAddr == "" {
// Headless mode
cfg, err := config.LoadFromFile(forkConfigFile)
cfg, err := cliutil.LoadFromFile(forkConfigFile)
if err != nil {
return fmt.Errorf("error loading config file '%s': %w", forkConfigFile, err)
}
Expand Down
15 changes: 14 additions & 1 deletion cmd/ax/internal/cliutil/cliutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,21 @@ type Controller = *controller.Controller
// ExecHandler is the handler type accepted by Controller.Exec.
type ExecHandler = controller.ExecHandler

// Config is the configuration type for this build.
type Config = config.Config

// LoadFromFile loads configuration from a YAML file.
func LoadFromFile(path string) (*Config, error) {
return config.LoadFromFile(path)
}

// DefaultConfig returns a configuration with default values set.
func DefaultConfig() *Config {
return config.DefaultConfig()
}

// NewControllerFromConfig creates a new Controller instance based on the provided configuration.
func NewControllerFromConfig(ctx context.Context, cfg *config.Config) (*controller.Controller, error) {
func NewControllerFromConfig(ctx context.Context, cfg *Config) (*controller.Controller, error) {
// Validate planner type early
switch cfg.Planner.Type {
case "gemini":
Expand Down
66 changes: 61 additions & 5 deletions cmd/ax/internal/cliutil/cliutil_harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ package cliutil

import (
"context"
"fmt"
"os"

"github.com/google/ax/internal/config"
"github.com/google/ax/internal/config2"
"github.com/google/ax/internal/controller/executor"
"github.com/google/ax/internal/controller2"
"github.com/google/ax/internal/harness"
)

// Controller is the active controller type for this build.
Expand All @@ -31,10 +32,65 @@ type Controller = *controller2.Controller
// ExecHandler is the handler type accepted by Controller.Exec.
type ExecHandler = controller2.ExecHandler

// NewControllerFromConfig creates a controller2.Controller.
func NewControllerFromConfig(ctx context.Context, cfg *config.Config) (*controller2.Controller, error) {
// Config is the configuration type for this build.
type Config = config2.Config

// LoadFromFile loads configuration from a YAML file.
func LoadFromFile(path string) (*Config, error) {
return config2.LoadFromFile(path)
}

// DefaultConfig returns a configuration with default values set.
func DefaultConfig() *Config {
return config2.DefaultConfig()
}

// NewControllerFromConfig creates a controller2.Controller instance based on the provided configuration.
func NewControllerFromConfig(ctx context.Context, cfg *Config) (*controller2.Controller, error) {
reg := controller2.NewRegistry()
reg.RegisterHarness("antigravity", harness.NewAntigravityHarness(""))

// AX_SUBSTRATE selects how built-in harnesses run: locally (unset) or as
// substrate actors ("1").
substrateMode := os.Getenv("AX_SUBSTRATE") == "1"
// AX_SUBSTRATE_ENDPOINT is the control-plane endpoint for substrate server.
endpoint := os.Getenv("AX_SUBSTRATE_ENDPOINT")

// Built-in harnesses.
for _, hc := range cfg.Harnesses.Antigravity {
h, err := hc.NewHarness(substrateMode, endpoint)
if err != nil {
return nil, fmt.Errorf("antigravity harness %q: %w", hc.ID, err)
}
if err := reg.RegisterHarness(hc.ID, h); err != nil {
return nil, fmt.Errorf("register antigravity harness %q: %w", hc.ID, err)
}
}

// Custom substrate harnesses.
if len(cfg.Harnesses.Substrate) > 0 && !substrateMode {
return nil, fmt.Errorf("custom substrate harnesses require AX_SUBSTRATE=1")
}
for _, sc := range cfg.Harnesses.Substrate {
h, err := sc.NewHarness(endpoint)
if err != nil {
return nil, fmt.Errorf("substrate harness %q: %w", sc.ID, err)
}
if err := reg.RegisterHarness(sc.ID, h); err != nil {
return nil, fmt.Errorf("register substrate harness %q: %w", sc.ID, err)
}
}

// Register the configured default harness.
if id := cfg.Harnesses.Default; id != "" {
h, err := reg.Harness(id)
if err != nil {
return nil, fmt.Errorf("default harness %q not found", id)
}
if err := reg.RegisterHarness("", h); err != nil {
return nil, fmt.Errorf("register default harness %q: %w", id, err)
}
}

return controller2.New(ctx, controller2.Config{
Registry: reg,
EventLogBuilder: func() (executor.EventLog, error) {
Expand Down
148 changes: 148 additions & 0 deletions cmd/ax/internal/cliutil/cliutil_harness_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//go:build harness

// 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.

package cliutil

import (
"context"
"path/filepath"
"strings"
"testing"

"github.com/google/ax/internal/config2"
)

func TestNewControllerFromConfig_DefaultHarness(t *testing.T) {
cfg := &config2.Config{
EventLog: config2.EventLogConfig{
SQLiteConfig: config2.SQLiteConfig{
Filename: filepath.Join(t.TempDir(), "log.sqlite"),
},
},
Harnesses: config2.HarnessesConfig{
Default: "ag",
Antigravity: []config2.AntigravityHarnessConfig{
{ID: "ag", Address: "localhost:50053"},
},
},
}

c, err := NewControllerFromConfig(context.Background(), cfg)
if err != nil {
t.Fatalf("NewControllerFromConfig: %v", err)
}
if c == nil {
t.Fatal("expected non-nil controller")
}
c.Close()
}

func TestNewControllerFromConfig_UnknownDefaultHarness(t *testing.T) {
cfg := &config2.Config{
Harnesses: config2.HarnessesConfig{
Default: "missing",
Antigravity: []config2.AntigravityHarnessConfig{
{ID: "ag", Address: "localhost:50053"},
},
},
}

_, err := NewControllerFromConfig(context.Background(), cfg)
if err == nil {
t.Fatal("expected error for unknown default harness, got nil")
}
if !strings.Contains(err.Error(), "missing") {
t.Errorf("expected error to mention %q, got: %v", "missing", err)
}
}

func TestNewControllerFromConfig_BuiltinSubstrate(t *testing.T) {
t.Setenv("AX_SUBSTRATE", "1")

cfg := &config2.Config{
EventLog: config2.EventLogConfig{
SQLiteConfig: config2.SQLiteConfig{
Filename: filepath.Join(t.TempDir(), "log.sqlite"),
},
},
Harnesses: config2.HarnessesConfig{
Default: "ag",
Antigravity: []config2.AntigravityHarnessConfig{
{ID: "ag"},
},
},
}

c, err := NewControllerFromConfig(context.Background(), cfg)
if err != nil {
t.Fatalf("NewControllerFromConfig: %v", err)
}
if c == nil {
t.Fatal("expected non-nil controller")
}
c.Close()
}

func TestNewControllerFromConfig_CustomHarnessRequiresSubstrateMode(t *testing.T) {
t.Setenv("AX_SUBSTRATE", "")

cfg := &config2.Config{
EventLog: config2.EventLogConfig{
SQLiteConfig: config2.SQLiteConfig{
Filename: filepath.Join(t.TempDir(), "log.sqlite"),
},
},
Harnesses: config2.HarnessesConfig{
Substrate: []config2.SubstrateHarnessConfig{
{ID: "custom", Namespace: "team-ns", Template: "custom-template"},
},
},
}

_, err := NewControllerFromConfig(context.Background(), cfg)
if err == nil {
t.Fatal("expected error for custom substrate harness without AX_SUBSTRATE=1, got nil")
}
if !strings.Contains(err.Error(), "AX_SUBSTRATE=1") {
t.Errorf("expected error to mention AX_SUBSTRATE=1, got: %v", err)
}
}

func TestNewControllerFromConfig_CustomHarnessInSubstrateMode(t *testing.T) {
t.Setenv("AX_SUBSTRATE", "1")

cfg := &config2.Config{
EventLog: config2.EventLogConfig{
SQLiteConfig: config2.SQLiteConfig{
Filename: filepath.Join(t.TempDir(), "log.sqlite"),
},
},
Harnesses: config2.HarnessesConfig{
Substrate: []config2.SubstrateHarnessConfig{
{ID: "custom", Namespace: "team-ns", Template: "custom-template"},
},
},
}

c, err := NewControllerFromConfig(context.Background(), cfg)
if err != nil {
t.Fatalf("NewControllerFromConfig: %v", err)
}
if c == nil {
t.Fatal("expected non-nil controller")
}
c.Close()
}
8 changes: 4 additions & 4 deletions cmd/ax/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
"fmt"
"os"

"github.com/google/ax/internal/config"
"github.com/google/ax/cmd/ax/internal/cliutil"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
Expand Down Expand Up @@ -61,10 +61,10 @@ func connect(server string) (*grpc.ClientConn, error) {
return conn, nil
}

func newConfig(cmd *cobra.Command, configFile string) (*config.Config, error) {
cfg, err := config.LoadFromFile(configFile)
func newConfig(cmd *cobra.Command, configFile string) (*cliutil.Config, error) {
cfg, err := cliutil.LoadFromFile(configFile)
if errors.Is(err, os.ErrNotExist) && !cmd.Flags().Changed("config") {
return config.DefaultConfig(), nil
return cliutil.DefaultConfig(), nil
}
if err != nil {
return nil, fmt.Errorf("error loading config file '%s': %w", configFile, err)
Expand Down
6 changes: 3 additions & 3 deletions cmd/ax/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"runtime"
"time"

"github.com/google/ax/internal/config"
"github.com/google/ax/cmd/ax/internal/cliutil"
"github.com/google/ax/internal/controller/executor"
"github.com/google/ax/proto"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -115,7 +115,7 @@ type TraceData struct {
Execs []ExecTrace `json:"execs"`
}

func loadTraceData(ctx context.Context, cfg *config.Config, convID string) (*TraceData, error) {
func loadTraceData(ctx context.Context, cfg *cliutil.Config, convID string) (*TraceData, error) {
events, rootExecID, execIDs, err := fetch(ctx, cfg, convID)
if err != nil {
return nil, err
Expand All @@ -132,7 +132,7 @@ func loadTraceData(ctx context.Context, cfg *config.Config, convID string) (*Tra
}, nil
}

func fetch(ctx context.Context, cfg *config.Config, convID string) ([]*proto.ExecutionEvent, string, []string, error) {
func fetch(ctx context.Context, cfg *cliutil.Config, convID string) ([]*proto.ExecutionEvent, string, []string, error) {
evLog, err := executor.OpenSQLiteEventLog(cfg.EventLog.SQLiteConfig.Filename)
if err != nil {
return nil, "", nil, fmt.Errorf("could not open sqlite eventlog: %w", err)
Expand Down
30 changes: 30 additions & 0 deletions internal/ax2.yaml
Original file line number Diff line number Diff line change
@@ -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.

# Sample LOCAL configuration for the AX *harness* build (compiled with -tags=harness).
# Usage: ax serve --config internal/ax2.yaml
# ax exec --config internal/ax2.yaml --input "hello" # uses harnesses.default

server:
address: ":8494"

eventlog:
sqlite:
filename: "eventlog/log.sqlite"

harnesses:
default: antigravity-example

antigravity:
- id: antigravity-example
Loading
Loading