From 066d40a14c3c741ec9a73acc55ffa3c13a5dfc69 Mon Sep 17 00:00:00 2001 From: Junjie Wang Date: Tue, 9 Jun 2026 17:03:36 -0700 Subject: [PATCH 1/5] Add health check for harness server. --- cmd/ax/harness.go | 7 ++ internal/harness/substrate.go | 50 +++++++++- internal/harness/substrate_test.go | 115 ++++++++++++++++++++++ python/antigravity/harness_server.py | 6 ++ python/antigravity/harness_server_test.py | 22 +++++ python/antigravity/requirements.txt | 9 ++ 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 internal/harness/substrate_test.go create mode 100644 python/antigravity/requirements.txt diff --git a/cmd/ax/harness.go b/cmd/ax/harness.go index 3faf6c9..89a5858 100644 --- a/cmd/ax/harness.go +++ b/cmd/ax/harness.go @@ -30,6 +30,8 @@ import ( "github.com/google/ax/proto" "github.com/spf13/cobra" "google.golang.org/grpc" + "google.golang.org/grpc/health" + "google.golang.org/grpc/health/grpc_health_v1" ) var ( @@ -59,6 +61,11 @@ func runHarness(cmd *cobra.Command, args []string) error { harnessServer := NewHarnessServiceServer() proto.RegisterHarnessServiceServer(grpcServer, harnessServer) + // Serve the standard gRPC health protocol. + healthServer := health.NewServer() + healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) + grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) + // Graceful shutdown handling sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) diff --git a/internal/harness/substrate.go b/internal/harness/substrate.go index a320915..fe61f40 100644 --- a/internal/harness/substrate.go +++ b/internal/harness/substrate.go @@ -28,6 +28,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/status" "github.com/google/ax/internal/experimental/k8s/ate" @@ -35,6 +36,10 @@ import ( "github.com/google/uuid" ) +// healthCheckTimeout defines the maximum time Start waits for a freshly +// created/resumed actor's harness to become reachable and ready. +const healthCheckTimeout = 60 * time.Second + // SubstrateHarness manages execution in a SubstrATE sandboxed actor over gRPC HarnessService. type SubstrateHarness struct { harnessID string @@ -95,13 +100,20 @@ func (h *SubstrateHarness) Start(ctx context.Context, conversationID string) (Ex return nil, fmt.Errorf("actor %s has no active worker IP address", conversationID) } - // 2. Establish connection to the actor's worker IP + // Establish connection to the actor's worker IP workerAddr := fmt.Sprintf("%s:%d", actor.AteomPodIp, h.port) conn, err := grpc.NewClient(workerAddr, h.dialOpts...) if err != nil { return nil, fmt.Errorf("failed to dial remote harness service at %s: %w", workerAddr, err) } + // Wait for the harness to be reachable and ready before handing back the + // execution. + if err := waitForHealthy(ctx, conn, healthCheckTimeout); err != nil { + conn.Close() + return nil, fmt.Errorf("harness for %s not ready at %s: %w", conversationID, workerAddr, err) + } + return &substrateExecution{ harness: h, conversationID: conversationID, @@ -111,6 +123,42 @@ func (h *SubstrateHarness) Start(ctx context.Context, conversationID string) (Ex }, nil } +// waitForHealthy blocks until the harness behind conn reports SERVING via the +// standard gRPC health protocol until timeout. A harness that is reachable +// but does not implement the health service (Unimplemented) is treated as +// ready; connection failures (Unavailable) and NOT_SERVING are retried. +func waitForHealthy(ctx context.Context, conn *grpc.ClientConn, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + client := grpc_health_v1.NewHealthClient(conn) + const maxBackoff = 2 * time.Second + backoff := 100 * time.Millisecond + for { + resp, err := client.Check(ctx, &grpc_health_v1.HealthCheckRequest{Service: ""}) + if err == nil && resp.GetStatus() == grpc_health_v1.HealthCheckResponse_SERVING { + return nil + } + if status.Code(err) == codes.Unimplemented { + // Reachable but no health service: the port is up, proceed. + return nil + } + + select { + case <-ctx.Done(): + if err != nil { + return fmt.Errorf("harness not healthy within %s: %w", timeout, err) + } + return fmt.Errorf("harness not healthy within %s (last status: %s)", timeout, resp.GetStatus()) + case <-time.After(backoff): + } + backoff *= 2 + if backoff > maxBackoff { + backoff = maxBackoff + } + } +} + type substrateExecution struct { harness *SubstrateHarness conversationID string diff --git a/internal/harness/substrate_test.go b/internal/harness/substrate_test.go new file mode 100644 index 0000000..0ec3d3b --- /dev/null +++ b/internal/harness/substrate_test.go @@ -0,0 +1,115 @@ +// 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 harness + +import ( + "context" + "net" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/health" + "google.golang.org/grpc/health/grpc_health_v1" +) + +// startHealthTestServer starts a gRPC server on a random local port. If hs is +// non-nil the standard health service is registered. Returns the listen address. +func startHealthTestServer(t *testing.T, hs *health.Server) string { + t.Helper() + lis, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + if hs != nil { + grpc_health_v1.RegisterHealthServer(s, hs) + } + go func() { + _ = s.Serve(lis) + }() + t.Cleanup(s.Stop) + return lis.Addr().String() +} + +func dialTestConn(t *testing.T, addr string) *grpc.ClientConn { + t.Helper() + conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("failed to dial %s: %v", addr, err) + } + t.Cleanup(func() { conn.Close() }) + return conn +} + +func TestWaitForHealthy_Serving(t *testing.T) { + hs := health.NewServer() + hs.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) + conn := dialTestConn(t, startHealthTestServer(t, hs)) + + if err := waitForHealthy(context.Background(), conn, 5*time.Second); err != nil { + t.Fatalf("expected healthy, got %v", err) + } +} + +func TestWaitForHealthy_UnimplementedProceeds(t *testing.T) { + // Server is up but does not register the health service. + conn := dialTestConn(t, startHealthTestServer(t, nil)) + + if err := waitForHealthy(context.Background(), conn, 5*time.Second); err != nil { + t.Fatalf("expected to proceed when health is unimplemented, got %v", err) + } +} + +func TestWaitForHealthy_TimesOut(t *testing.T) { + hs := health.NewServer() + hs.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING) + conn := dialTestConn(t, startHealthTestServer(t, hs)) + + if err := waitForHealthy(context.Background(), conn, 500*time.Millisecond); err == nil { + t.Fatal("expected timeout error while NOT_SERVING, got nil") + } +} + +func TestWaitForHealthy_StatusChange(t *testing.T) { + hs := health.NewServer() + hs.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING) + conn := dialTestConn(t, startHealthTestServer(t, hs)) + + go func() { + time.Sleep(150 * time.Millisecond) + hs.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) + }() + + if err := waitForHealthy(context.Background(), conn, 5*time.Second); err != nil { + t.Fatalf("expected healthy after status flip, got %v", err) + } +} + +func TestWaitForHealthy_ServerDown(t *testing.T) { + // Reserve a port then release it so nothing is listening there. + lis, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + addr := lis.Addr().String() + lis.Close() + conn := dialTestConn(t, addr) + + if err := waitForHealthy(context.Background(), conn, 500*time.Millisecond); err == nil { + t.Fatal("expected timeout error when server is down, got nil") + } +} diff --git a/python/antigravity/harness_server.py b/python/antigravity/harness_server.py index ae34a7b..8f7513f 100644 --- a/python/antigravity/harness_server.py +++ b/python/antigravity/harness_server.py @@ -24,6 +24,7 @@ import logging import sys import grpc +from grpc_health.v1 import health, health_pb2, health_pb2_grpc from google.protobuf.struct_pb2 import Struct from python.proto import ax_pb2 @@ -210,6 +211,11 @@ async def _run_turn(self, request): async def serve(host: str, port: int): server = grpc.aio.server() ax_pb2_grpc.add_HarnessServiceServicer_to_server(AntigravityHarnessServiceServicer(), server) + + # Serve the standard gRPC health protocol. + health_servicer = health.aio.HealthServicer() + health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server) + await health_servicer.set("", health_pb2.HealthCheckResponse.SERVING) listen_addr = f"{host}:{port}" server.add_insecure_port(listen_addr) diff --git a/python/antigravity/harness_server_test.py b/python/antigravity/harness_server_test.py index 8b4baf3..ae14f6c 100644 --- a/python/antigravity/harness_server_test.py +++ b/python/antigravity/harness_server_test.py @@ -93,3 +93,25 @@ async def request_iter(): await server.stop(0) asyncio.run(_run()) + + +def test_health_check(): + async def _run(): + from grpc_health.v1 import health, health_pb2, health_pb2_grpc + + server = grpc.aio.server() + ax_pb2_grpc.add_HarnessServiceServicer_to_server(AntigravityHarnessServiceServicer(), server) + health_servicer = health.aio.HealthServicer() + health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server) + await health_servicer.set("", health_pb2.HealthCheckResponse.SERVING) + port = server.add_insecure_port("localhost:0") + await server.start() + try: + async with grpc.aio.insecure_channel(f"localhost:{port}") as channel: + stub = health_pb2_grpc.HealthStub(channel) + resp = await stub.Check(health_pb2.HealthCheckRequest(service="")) + assert resp.status == health_pb2.HealthCheckResponse.SERVING + finally: + await server.stop(0) + + asyncio.run(_run()) diff --git a/python/antigravity/requirements.txt b/python/antigravity/requirements.txt new file mode 100644 index 0000000..dd9e44e --- /dev/null +++ b/python/antigravity/requirements.txt @@ -0,0 +1,9 @@ +# Runtime dependencies for the antigravity HarnessService server +# (python/antigravity/harness_server.py). +# +# grpcio-health-checking is published on public PyPI but not the internal +# mirror, so allow PyPI as an extra index for it. +--extra-index-url https://pypi.org/simple/ + +google-antigravity +grpcio-health-checking==1.81.0 From f15c674d9fb7e730e3de5742b2c02f60b013b2af Mon Sep 17 00:00:00 2001 From: Junjie Wang Date: Thu, 11 Jun 2026 18:25:31 -0700 Subject: [PATCH 2/5] Deploy the actual antigravity on substrate. --- cmd/axepp/main.go | 2 +- go.mod | 19 ++-- go.sum | 72 ++++++++++--- internal/config2/config.go | 2 +- internal/experimental/k8s/ate/client.go | 2 +- internal/hack/install-ax.sh | 130 +++++++++++++++++++++++- internal/manifests/README.md | 72 ++++++++++--- internal/manifests/ax-deployment2.yaml | 26 +++-- python/antigravity/Dockerfile | 61 +++++++++++ python/antigravity/harness_server.py | 22 ++++ python/antigravity/requirements.txt | 2 +- 11 files changed, 356 insertions(+), 54 deletions(-) create mode 100644 python/antigravity/Dockerfile diff --git a/cmd/axepp/main.go b/cmd/axepp/main.go index 207cd0f..8c8c373 100644 --- a/cmd/axepp/main.go +++ b/cmd/axepp/main.go @@ -29,7 +29,7 @@ import ( "net" "os" - "github.com/agent-substrate/substrate/proto/ateapipb" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" "github.com/google/ax/proto" diff --git a/go.mod b/go.mod index 47fc344..55a3123 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/google/ax -go 1.26.1 +go 1.26.3 require ( charm.land/huh/v2 v2.0.3 charm.land/lipgloss/v2 v2.0.3 github.com/a2aproject/a2a-go/v2 v2.2.0 - github.com/agent-substrate/substrate v0.0.0 + github.com/agent-substrate/substrate v0.0.0-20260611053709-a3f44744d37b github.com/envoyproxy/go-control-plane/envoy v1.37.0 github.com/google/uuid v1.6.0 github.com/spf13/cobra v1.10.2 @@ -45,6 +45,7 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/nftables v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.21.0 // indirect @@ -54,6 +55,8 @@ require ( github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect + github.com/mdlayher/socket v0.5.0 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect @@ -65,15 +68,15 @@ require ( github.com/vishvananda/netns v0.0.5 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.52.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect google.golang.org/api v0.274.0 // indirect modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index bb1ac88..ec5c8bb 100644 --- a/go.sum +++ b/go.sum @@ -16,14 +16,20 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/a2aproject/a2a-go/v2 v2.2.0 h1:eayiNXYpyTOLVhhQrGmIHlcy8GnOdnwaNdYQPvS84Ik= github.com/a2aproject/a2a-go/v2 v2.2.0/go.mod h1:htTxMwicNXXXEwwfjuB/Pd1g7UHDrswhSievncmTVcE= -github.com/agent-substrate/substrate v0.0.0 h1:XEX4QAjzaIcv4amBqBvPE/f40WV5WHRWo7u04xvqv/g= -github.com/agent-substrate/substrate v0.0.0/go.mod h1:8Z4SJqPWDMPBa76JgIdpiX0jTY1JXcfLTXEAtkUv7go= +github.com/agent-substrate/substrate v0.0.0-20260610233805-8c64c7bf3a4f h1:oFLOlBmMf74qiGW0R9P1oQsI7m+iCu2Zb9+GJNVdH6c= +github.com/agent-substrate/substrate v0.0.0-20260610233805-8c64c7bf3a4f/go.mod h1:ZPtBipt+cON5CWwPGtRKd8mlF7+vk8YaPRs9gHay5Io= +github.com/agent-substrate/substrate v0.0.0-20260611053709-a3f44744d37b h1:fpoOQUZ1CV3v10q50dP9eGXzrnltC14DQdKP8269L/s= +github.com/agent-substrate/substrate v0.0.0-20260611053709-a3f44744d37b/go.mod h1:TgdtEUV6iaflJTwmS8ONiGsyyJD+5okPZj2H6mM8WlA= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= @@ -78,6 +84,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg= +github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM= github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e h1:FJta/0WsADCe1r9vQjdHbd3KuiLPu7Y9WlyLGwMUNyE= github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -90,6 +98,8 @@ github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/go-reap v0.0.0-20260220095743-4e27870b4f51 h1:MpKgm7VEcOAD3dIR+cRoK4rbCcjqYXsMGCnFWTcHfds= github.com/hashicorp/go-reap v0.0.0-20260220095743-4e27870b4f51/go.mod h1:qIFzeFcJU3OIFk/7JreWXcUjFmcCaeHTH9KoNyHYVCs= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -106,16 +116,32 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= +github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= +github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= +github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -138,10 +164,20 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE= +go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= @@ -150,26 +186,30 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.274.0 h1:aYhycS5QQCwxHLwfEHRRLf9yNsfvp1JadKKWBE54RFA= diff --git a/internal/config2/config.go b/internal/config2/config.go index cfaa9ed..218f810 100644 --- a/internal/config2/config.go +++ b/internal/config2/config.go @@ -27,7 +27,7 @@ const ( // The substrate namespace reserved for AX's built-in harnesses. defaultNamespace = "ax" // The default port of HarnessService. - defaultPort = 50053 + defaultPort = 80 // The Antigravity ActorTemplate name. antigravityTemplate = "antigravity-template" ) diff --git a/internal/experimental/k8s/ate/client.go b/internal/experimental/k8s/ate/client.go index 4237285..6a71004 100644 --- a/internal/experimental/k8s/ate/client.go +++ b/internal/experimental/k8s/ate/client.go @@ -19,7 +19,7 @@ import ( "context" "fmt" - "github.com/agent-substrate/substrate/proto/ateapipb" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) diff --git a/internal/hack/install-ax.sh b/internal/hack/install-ax.sh index c7f53e8..d0a54a7 100755 --- a/internal/hack/install-ax.sh +++ b/internal/hack/install-ax.sh @@ -20,6 +20,18 @@ set -o pipefail ROOT=$(git rev-parse --show-toplevel) cd "${ROOT}" +# Directory holding the pre-downloaded linux/amd64 wheels used to build the +# antigravity harness image (including the google-antigravity wheel that bundles +# the localharness binary). Populate it with `install-ax.sh --fetch-wheels` +# (delete the directory first to refresh from scratch). Override the location +# with the WHEELS_DIR env var. +WHEELS_DIR="${WHEELS_DIR:-${HOME}/.cache/ax-antigravity-wheels}" + +# Python interpreter used by --fetch-wheels. Any Python 3 with pip works; the +# downloaded wheels always target the container's Python (3.13) regardless of +# this interpreter's own version. Override with the PYTHON env var. +PYTHON="${PYTHON:-python3}" + # ANSI color codes for prettier output COLOR_CYAN='\033[1;36m' COLOR_RESET='\033[0m' @@ -33,6 +45,7 @@ function usage() { echo "Usage: $0 [options]" echo "" echo "Options:" + echo " --fetch-wheels Download the antigravity harness wheels into WHEELS_DIR" echo " --deploy-ax-server Deploy AX server and components using ko" echo " --delete-ax-server Delete AX server and components from cluster" echo " -h, --help Show this help message" @@ -50,6 +63,114 @@ run_ko() { "$@" } +# detect_container_engine selects the OCI build/push tool when CONTAINER_ENGINE +# is not set explicitly. It prefers a *working* docker (daemon reachable), then a +# working podman, so a docker CLI installed without a running daemon does not +# shadow a working podman. As a last resort it picks whichever CLI exists so the +# build step can surface an actionable daemon error. +detect_container_engine() { + if [[ -n "${CONTAINER_ENGINE:-}" ]]; then + return # Respect an explicit override; do not second-guess it. + fi + if docker info >/dev/null 2>&1; then + CONTAINER_ENGINE=docker + elif podman info >/dev/null 2>&1; then + CONTAINER_ENGINE=podman + elif command -v docker >/dev/null 2>&1; then + CONTAINER_ENGINE=docker + elif command -v podman >/dev/null 2>&1; then + CONTAINER_ENGINE=podman + else + CONTAINER_ENGINE=docker + fi +} + +# fetch_wheels downloads the antigravity harness wheel closure into WHEELS_DIR. +# Resolution targets linux/amd64 + CPython 3.13 regardless of the host OS or host +# Python. +fetch_wheels() { + log_step "fetch_wheels -> ${WHEELS_DIR}" + + if ! "${PYTHON}" -m pip --version >/dev/null 2>&1; then + echo "Error: '${PYTHON} -m pip' is not available." >&2 + echo "Install pip or set PYTHON to an interpreter that has it." >&2 + exit 1 + fi + + mkdir -p "${WHEELS_DIR}" + + "${PYTHON}" -m pip download \ + --only-binary=:all: \ + --python-version 3.13 \ + --platform manylinux_2_17_x86_64 \ + --platform manylinux2014_x86_64 \ + --platform manylinux_2_28_x86_64 \ + --platform manylinux1_x86_64 \ + --platform linux_x86_64 \ + -r python/antigravity/requirements.txt \ + -d "${WHEELS_DIR}" + + echo "Wheel cache ready: ${WHEELS_DIR}" +} + +# build_antigravity_image builds and pushes the antigravity harness image and +# echoes its digest-pinned reference on stdout. Requires KO_DOCKER_REPO, +# a container engine, and a populated wheel cache. +build_antigravity_image() { + if [[ -z "${KO_DOCKER_REPO:-}" ]]; then + echo "Error: KO_DOCKER_REPO environment variable must be set" >&2 + exit 1 + fi + detect_container_engine + if ! command -v "${CONTAINER_ENGINE}" >/dev/null 2>&1; then + echo "Error: container engine '${CONTAINER_ENGINE}' not found in PATH." >&2 + echo "Install it or set CONTAINER_ENGINE to an available builder." >&2 + exit 1 + fi + if [[ ! -d "${WHEELS_DIR}" ]] || [[ -z "$(ls -A "${WHEELS_DIR}" 2>/dev/null)" ]]; then + echo "Error: antigravity wheel cache '${WHEELS_DIR}' is missing or empty." >&2 + echo "Run '$0 --fetch-wheels' to populate it (or set WHEELS_DIR)." >&2 + exit 1 + fi + + local repo tag image digest + repo="${KO_DOCKER_REPO}/ax-antigravity-harness" + tag="$(git rev-parse --short HEAD)" + image="${repo}:${tag}" + + # The cluster runs on linux/amd64 and the bundled localharness is an amd64 + # binary, so the image must be amd64 regardless of the build host. + log_step "build_antigravity_image -> ${image}" >&2 + "${CONTAINER_ENGINE}" build \ + --platform linux/amd64 \ + --build-context "wheels=${WHEELS_DIR}" \ + -f python/antigravity/Dockerfile \ + -t "${image}" \ + . >&2 + + # Push the readable tag, then resolve the pushed manifest digest so the + # ActorTemplate can reference the image by digest (snapshot-safe). + if [[ "${CONTAINER_ENGINE}" == *podman* ]]; then + local digestfile + digestfile="$(mktemp)" + "${CONTAINER_ENGINE}" push --digestfile="${digestfile}" "${image}" >&2 + digest="$(cat "${digestfile}")" + rm -f "${digestfile}" + else + "${CONTAINER_ENGINE}" push "${image}" >&2 + local repo_digest + repo_digest="$("${CONTAINER_ENGINE}" image inspect --format '{{index .RepoDigests 0}}' "${image}")" + digest="${repo_digest##*@}" + fi + + if [[ "${digest}" != sha256:* ]]; then + echo "Error: could not resolve a sha256 digest for ${image} (got '${digest}')." >&2 + exit 1 + fi + + echo "${repo}@${digest}" +} + deploy_ax_server() { log_step "deploy_ax_server" @@ -65,9 +186,14 @@ deploy_ax_server() { echo "Using GCS Bucket: ${BUCKET_NAME}" + # Build and push the antigravity harness image, capturing its reference. + local antigravity_image + antigravity_image=$(build_antigravity_image) + # Render template and apply with ko sed -e "s|\${GEMINI_API_KEY}|${GEMINI_API_KEY}|g" \ -e "s|\${BUCKET_NAME}|${BUCKET_NAME}|g" \ + -e "s|\${ANTIGRAVITY_IMAGE}|${antigravity_image}|g" \ internal/manifests/ax-deployment2.yaml \ | run_ko -f - } @@ -75,9 +201,10 @@ deploy_ax_server() { delete_ax_server() { log_step "delete_ax_server" - # Delete resources using a dummy key and bucket so credentials aren't required for deletion + # Delete resources using dummy values so credentials aren't required for deletion sed -e "s|\${GEMINI_API_KEY}|dummy-key|g" \ -e "s|\${BUCKET_NAME}|dummy-bucket|g" \ + -e "s|\${ANTIGRAVITY_IMAGE}|dummy-image|g" \ internal/manifests/ax-deployment2.yaml \ | run_kubectl delete --ignore-not-found -f - } @@ -99,6 +226,7 @@ done while [[ "$#" -gt 0 ]]; do case $1 in + --fetch-wheels) fetch_wheels ;; --deploy-ax-server) deploy_ax_server ;; --delete-ax-server) delete_ax_server ;; *) diff --git a/internal/manifests/README.md b/internal/manifests/README.md index faa090f..af950f4 100644 --- a/internal/manifests/README.md +++ b/internal/manifests/README.md @@ -38,9 +38,48 @@ This deploys the AX `harness` path: a built-in harness `WorkerPool` and `ActorTe ### 1. Build and Deploy > [!NOTE] -> Do not manually edit `internal/manifests/ax-deployment2.yaml`. The installation script automatically injects your `${GEMINI_API_KEY}` and `${BUCKET_NAME}` environment variables during deployment. +> Do not manually edit `internal/manifests/ax-deployment2.yaml`. The installation script automatically injects your `${GEMINI_API_KEY}`, `${BUCKET_NAME}`, and the built `${ANTIGRAVITY_IMAGE}` reference during deployment. -Use the installation script to build the images (with the `harness` build tag) and apply the resolved manifests to your cluster: +The installation script builds two images and applies the resolved manifests to +your cluster: the AX control-plane (Go) image, built with `ko` using the +`harness` build tag, and the built-in **antigravity harness** image, built from +`python/antigravity/Dockerfile` with Docker or Podman. + +#### Build prerequisites + +The antigravity image bundles the antigravity SDK and its `localharness` binary, +installed offline from a pre-downloaded linux/amd64 wheel cache. Fetch it once +(re-run after dependency changes): + +```bash +./internal/hack/install-ax.sh --fetch-wheels +``` + +> [!NOTE] +> `--fetch-wheels` resolves the **linux/amd64 + CPython 3.13** wheels regardless +> of your host OS/Python, so Mac and Linux produce the same set. It uses your +> host pip index configuration, which must reach the private antigravity registry +> (override the primary index with `PIP_INDEX_URL`). Customize the cache location +> with `WHEELS_DIR` and the interpreter with `PYTHON`. + +You also need a container engine to build and push the harness image. The script +auto-detects one (preferring a **running** docker, then podman); force a choice +with `CONTAINER_ENGINE=docker` or `CONTAINER_ENGINE=podman`. The engine must +support `--build-context` and `RUN --mount`: + +- **Docker** — Docker Desktop (macOS; cross-builds linux/amd64 via emulation) or + Docker Engine (Linux; native). Requires BuildKit (default since Docker 23; on + older Docker use `docker buildx`). Authenticate to your registry with + `gcloud auth configure-docker -docker.pkg.dev` or `docker login`. +- **Podman** — on macOS, start a machine first with `podman machine init && + podman machine start` (cross-builds linux/amd64 via emulation); on Linux it + runs natively (podman/buildah >= 4.0). Authenticate with a credential helper + or `podman login`. + +Unlike `ko`, the container engine's `push` is not auto-authenticated, so make +sure you are logged in to `$KO_DOCKER_REPO` first. + +#### Deploy ```bash export PROJECT_ID="ax-substrate" # Your GCP project ID @@ -53,7 +92,11 @@ export KO_DEFAULTPLATFORMS="linux/amd64" ``` This command will: -- Build the AX images using `ko` with the `harness` build tag. +- Build the AX control-plane image with `ko` (`harness` build tag) and the + antigravity harness image with the detected container engine, pushing both to + `$KO_DOCKER_REPO`. Both are referenced by **digest** (`repo@sha256:...`) in the + `ActorTemplate`, which Substrate requires because a moving tag would invalidate + the actor's live snapshots. - Create the `ax` namespace (AX control plane + built-in harnesses) and the `custom-harness` namespace (the example custom harness). - Create a shared `ax-harness-workerpool` `WorkerPool` and the built-in @@ -66,11 +109,10 @@ This command will: - Create the `ax-server-config` `ConfigMap` that tells the `ax-server` which harnesses to serve (mounted at `/etc/ax/ax.yaml`). -The harness registry lives in that `ConfigMap`. It registers a built-in -`antigravity` harness (AX-managed, in `ax`; currently a placeholder stub that -returns "hello world" until the real antigravity image lands) and a custom -substrate harness (`hello-world`, in `custom-harness`), with the latter marked as -the default via `harnesses.default`. +The harness registry lives in that `ConfigMap`. It registers the built-in +`antigravity` harness (AX-managed, in `ax`; the antigravity image built +above) and a custom substrate harness (`hello-world`, in `custom-harness`), with +`antigravity` marked as the default via `harnesses.default`. Wait until the templates are ready: ```bash @@ -89,21 +131,23 @@ kubectl port-forward -n ax rs/ax-server 8494:8494 ### 3. Test End-to-End -Run an execution targeting the port-forwarded server: +Run an execution targeting the port-forwarded server. The default `antigravity` +harness serves the example `examples/antigravity_agent/agent.py`, which exposes +a `get_weather` tool. ```bash -ax exec --server=localhost:8494 --input="hello" +ax exec --server=localhost:8494 --input="what's the weather in NYC?" ``` -The server should respond with: +The server should respond with something like: ```text Conversation: fb344a18-3720-4c4f-8a6e-2ce34db975b3 -⏺ hello +⏺ what's the weather in NYC? -hello world +The weather in New York is sunny with a temperature of 25 degrees Celsius (77 degrees Fahrenheit). ``` -*The request is served by the harness actor running on Substrate.* +*The request is served by the antigravity harness actor running on Substrate.* ## 🧹 How to Uninstall diff --git a/internal/manifests/ax-deployment2.yaml b/internal/manifests/ax-deployment2.yaml index 3657912..c8b311a 100644 --- a/internal/manifests/ax-deployment2.yaml +++ b/internal/manifests/ax-deployment2.yaml @@ -39,7 +39,7 @@ metadata: namespace: ax spec: replicas: 5 - ateomImage: ko://github.com/agent-substrate/substrate/cmd/servers/ateom-gvisor + ateomImage: ko://github.com/agent-substrate/substrate/cmd/ateom-gvisor --- apiVersion: ate.dev/v1alpha1 kind: ActorTemplate @@ -59,12 +59,18 @@ spec: sha256Hash: "1ba2366ae2efceba166046f51a4104f9261c9cb72c6db8f5b3fe2dc57dea86b9" pauseImage: "gcr.io/gke-release/pause@sha256:bcbd57ba5653580ec647b16d8163cdd1112df3609129b01f912a8032e48265da" containers: - # TODO(wjjclaud): update the hello-world demo container with real antigravity. - name: "axharness" - image: ko://github.com/google/ax/cmd/ax - command: ["/ko-app/ax", "harness"] - ports: - - containerPort: 50053 + image: ${ANTIGRAVITY_IMAGE} + command: ["python", "-m", "python.antigravity.harness_server", + "--agent_file", "/app/examples/antigravity_agent/agent.py", + "--host", "0.0.0.0", "--port", "80"] + env: + - name: GEMINI_API_KEY + value: "${GEMINI_API_KEY}" + - name: PYTHONPATH + value: "/app:/app/python" + - name: PYTHONUNBUFFERED + value: "1" snapshotsConfig: location: gs://${BUCKET_NAME}/antigravity/ --- @@ -78,7 +84,7 @@ metadata: namespace: custom-harness spec: replicas: 3 - ateomImage: ko://github.com/agent-substrate/substrate/cmd/servers/ateom-gvisor + ateomImage: ko://github.com/agent-substrate/substrate/cmd/ateom-gvisor --- apiVersion: ate.dev/v1alpha1 kind: ActorTemplate @@ -100,9 +106,8 @@ spec: containers: - name: "axharness" image: ko://github.com/google/ax/cmd/ax - command: ["/ko-app/ax", "harness"] - ports: - - containerPort: 50053 + # Serve on 80: substrate's actor networking only DNATs workerPodIP:80. + command: ["/ko-app/ax", "harness", "--port", "80"] snapshotsConfig: location: gs://${BUCKET_NAME}/hello-world/ --- @@ -164,4 +169,3 @@ data: - id: hello-world namespace: custom-harness template: hello-world-template - port: 50053 diff --git a/python/antigravity/Dockerfile b/python/antigravity/Dockerfile new file mode 100644 index 0000000..4f651aa --- /dev/null +++ b/python/antigravity/Dockerfile @@ -0,0 +1,61 @@ +# 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. + +# Container image for the Antigravity HarnessService server. +# +# Runtime dependencies -- including the linux/amd64 google-antigravity wheel that +# bundles the linux `localharness` binary -- are installed offline from a +# "wheels" build context, so the build needs no access to the private package +# registry. Populate the wheel cache with `internal/hack/install-ax.sh +# --fetch-wheels` (which also drives the build/deploy). +# +# Build context: repository root. Requires an OCI builder that supports +# additional build contexts and RUN --mount: docker with BuildKit (default since +# Docker 23; older Docker: use `docker buildx build`), or podman/buildah >= 4.0. +# For example: +# # Docker +# docker build --platform linux/amd64 \ +# --build-context wheels=$HOME/.cache/ax-antigravity-wheels \ +# -f python/antigravity/Dockerfile -t . && docker push +# # Podman +# podman build --platform linux/amd64 \ +# --build-context wheels=$HOME/.cache/ax-antigravity-wheels \ +# -f python/antigravity/Dockerfile -t . && podman push + +FROM python:3.13-slim + +WORKDIR /app + +# Install runtime deps offline from the pre-downloaded wheels. They are +# bind-mounted (not COPYed) so they never land in an image layer. +COPY python/antigravity/requirements.txt /tmp/requirements.txt +RUN --mount=type=bind,from=wheels,target=/tmp/wheels \ + pip install --no-cache-dir --no-index --find-links=/tmp/wheels -r /tmp/requirements.txt + +# Harness server (with generated proto stubs) and the agent it serves. +COPY python/ /app/python +COPY examples/antigravity_agent/ /app/examples/antigravity_agent + +# `from python.proto import ...` needs /app on the path; the generated stubs do +# `from proto import ...`, which needs /app/python. +ENV PYTHONPATH=/app:/app/python + +# Flush stdout/stderr immediately so server logs surface in container logs. +ENV PYTHONUNBUFFERED=1 + +EXPOSE 50053 + +CMD ["python", "-m", "python.antigravity.harness_server", \ + "--agent_file", "examples/antigravity_agent/agent.py", \ + "--host", "0.0.0.0", "--port", "50053"] diff --git a/python/antigravity/harness_server.py b/python/antigravity/harness_server.py index 8f7513f..c11165d 100644 --- a/python/antigravity/harness_server.py +++ b/python/antigravity/harness_server.py @@ -223,6 +223,26 @@ async def serve(host: str, port: int): await server.start() await server.wait_for_termination() +def resolve_localhost(): + """Ensure `localhost` resolves to 127.0.0.1. + + Substrate actors run under gVisor with no runtime-injected /etc/hosts. + The antigravity SDK dials localharness at ws://localhost:/ + and Python's resolver needs `localhost` in /etc/hosts. + """ + try: + try: + with open("/etc/hosts", "r") as f: + if "localhost" in f.read(): + return + except FileNotFoundError: + pass + with open("/etc/hosts", "a") as f: + f.write("127.0.0.1\tlocalhost\n") + except OSError as e: + print(f"WARNING: could not ensure localhost in /etc/hosts: {e}", file=sys.stderr) + + def main(): parser = argparse.ArgumentParser(description="Antigravity gRPC Harness Server") parser.add_argument("--agent_file", default="examples/antigravity_agent/agent.py", help="Path to the agent config file") @@ -230,6 +250,8 @@ def main(): parser.add_argument("--host", default="localhost", help="Host to bind the server to") args = parser.parse_args() + resolve_localhost() + # Load the agent config globally global loaded_config try: diff --git a/python/antigravity/requirements.txt b/python/antigravity/requirements.txt index dd9e44e..310b3f3 100644 --- a/python/antigravity/requirements.txt +++ b/python/antigravity/requirements.txt @@ -5,5 +5,5 @@ # mirror, so allow PyPI as an extra index for it. --extra-index-url https://pypi.org/simple/ -google-antigravity +google-antigravity==0.1.2 grpcio-health-checking==1.81.0 From 92d76170275a3a4fe70be77cc4d6405275126ffc Mon Sep 17 00:00:00 2001 From: Junjie Wang Date: Thu, 11 Jun 2026 18:40:56 -0700 Subject: [PATCH 3/5] Update port in dockerfile. --- python/antigravity/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/antigravity/Dockerfile b/python/antigravity/Dockerfile index 4f651aa..fe61001 100644 --- a/python/antigravity/Dockerfile +++ b/python/antigravity/Dockerfile @@ -54,8 +54,8 @@ ENV PYTHONPATH=/app:/app/python # Flush stdout/stderr immediately so server logs surface in container logs. ENV PYTHONUNBUFFERED=1 -EXPOSE 50053 +EXPOSE 80 CMD ["python", "-m", "python.antigravity.harness_server", \ "--agent_file", "examples/antigravity_agent/agent.py", \ - "--host", "0.0.0.0", "--port", "50053"] + "--host", "0.0.0.0", "--port", "80"] From 75a29d9af5dafd224ec668255536089de356cebd Mon Sep 17 00:00:00 2001 From: Junjie Wang Date: Fri, 12 Jun 2026 10:43:41 -0700 Subject: [PATCH 4/5] Add some comments and new default port for substrate. --- internal/config2/config.go | 11 +++++++---- internal/manifests/ax-deployment2.yaml | 3 +++ python/antigravity/Dockerfile | 2 ++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/config2/config.go b/internal/config2/config.go index 218f810..af5dfeb 100644 --- a/internal/config2/config.go +++ b/internal/config2/config.go @@ -26,8 +26,11 @@ import ( const ( // The substrate namespace reserved for AX's built-in harnesses. defaultNamespace = "ax" - // The default port of HarnessService. - defaultPort = 80 + // The default HarnessService port for non-substrate harnesses. + defaultPort = 50053 + // The port for harnesses running as substrate actors. Substrate's + // actor networking DNATs inbound workerPodIP:80 to the actor. + substrateDefaultPort = 80 // The Antigravity ActorTemplate name. antigravityTemplate = "antigravity-template" ) @@ -85,7 +88,7 @@ type SubstrateHarnessConfig struct { // as a substrate actor; otherwise it runs locally. func (c AntigravityHarnessConfig) NewHarness(substrate bool, endpoint string) (harness.Harness, error) { if substrate { - return newSubstrateHarness(c.ID, endpoint, defaultNamespace, antigravityTemplate, defaultPort) + return newSubstrateHarness(c.ID, endpoint, defaultNamespace, antigravityTemplate, substrateDefaultPort) } address := c.Address if address == "" { @@ -99,7 +102,7 @@ func (c AntigravityHarnessConfig) NewHarness(substrate bool, endpoint string) (h func (c SubstrateHarnessConfig) NewHarness(endpoint string) (harness.Harness, error) { port := c.Port if port == 0 { - port = defaultPort + port = substrateDefaultPort } return newSubstrateHarness(c.ID, endpoint, c.Namespace, c.Template, port) } diff --git a/internal/manifests/ax-deployment2.yaml b/internal/manifests/ax-deployment2.yaml index c8b311a..a8519ab 100644 --- a/internal/manifests/ax-deployment2.yaml +++ b/internal/manifests/ax-deployment2.yaml @@ -61,6 +61,9 @@ spec: containers: - name: "axharness" image: ${ANTIGRAVITY_IMAGE} + # Substrate ignores the image CMD/ENV/WORKDIR, so the harness command and + # environment are specified here. The harness serves on port 80 because + # substrate DNATs workerPodIP:80. command: ["python", "-m", "python.antigravity.harness_server", "--agent_file", "/app/examples/antigravity_agent/agent.py", "--host", "0.0.0.0", "--port", "80"] diff --git a/python/antigravity/Dockerfile b/python/antigravity/Dockerfile index fe61001..bdb7341 100644 --- a/python/antigravity/Dockerfile +++ b/python/antigravity/Dockerfile @@ -56,6 +56,8 @@ ENV PYTHONUNBUFFERED=1 EXPOSE 80 +# Command for local docker build and test. +# Substrate ignores the image CMD and runs the ActorTemplate `command` instead. CMD ["python", "-m", "python.antigravity.harness_server", \ "--agent_file", "examples/antigravity_agent/agent.py", \ "--host", "0.0.0.0", "--port", "80"] From e80bdf010bb9db3431f49321d28d4765e06d8826 Mon Sep 17 00:00:00 2001 From: Junjie Wang Date: Fri, 12 Jun 2026 11:05:19 -0700 Subject: [PATCH 5/5] Bump substrate version. --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 55a3123..b55ea3f 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( charm.land/huh/v2 v2.0.3 charm.land/lipgloss/v2 v2.0.3 github.com/a2aproject/a2a-go/v2 v2.2.0 - github.com/agent-substrate/substrate v0.0.0-20260611053709-a3f44744d37b + github.com/agent-substrate/substrate v0.0.0-20260612160914-27aa4eec85f2 github.com/envoyproxy/go-control-plane/envoy v1.37.0 github.com/google/uuid v1.6.0 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index ec5c8bb..26e50c5 100644 --- a/go.sum +++ b/go.sum @@ -16,10 +16,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/a2aproject/a2a-go/v2 v2.2.0 h1:eayiNXYpyTOLVhhQrGmIHlcy8GnOdnwaNdYQPvS84Ik= github.com/a2aproject/a2a-go/v2 v2.2.0/go.mod h1:htTxMwicNXXXEwwfjuB/Pd1g7UHDrswhSievncmTVcE= -github.com/agent-substrate/substrate v0.0.0-20260610233805-8c64c7bf3a4f h1:oFLOlBmMf74qiGW0R9P1oQsI7m+iCu2Zb9+GJNVdH6c= -github.com/agent-substrate/substrate v0.0.0-20260610233805-8c64c7bf3a4f/go.mod h1:ZPtBipt+cON5CWwPGtRKd8mlF7+vk8YaPRs9gHay5Io= -github.com/agent-substrate/substrate v0.0.0-20260611053709-a3f44744d37b h1:fpoOQUZ1CV3v10q50dP9eGXzrnltC14DQdKP8269L/s= -github.com/agent-substrate/substrate v0.0.0-20260611053709-a3f44744d37b/go.mod h1:TgdtEUV6iaflJTwmS8ONiGsyyJD+5okPZj2H6mM8WlA= +github.com/agent-substrate/substrate v0.0.0-20260612160914-27aa4eec85f2 h1:SlFLoJ3EPWrgR3bBlwCUmpJY6rqrAmvkwb1a9++WOBw= +github.com/agent-substrate/substrate v0.0.0-20260612160914-27aa4eec85f2/go.mod h1:TgdtEUV6iaflJTwmS8ONiGsyyJD+5okPZj2H6mM8WlA= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=