diff --git a/cmd/ax/Dockerfile b/cmd/ax/Dockerfile new file mode 100644 index 0000000..889fb9a --- /dev/null +++ b/cmd/ax/Dockerfile @@ -0,0 +1,76 @@ +# 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. + +# Comprehensive AX image: the Go `ax` binary plus the Antigravity Python sidecar +# (SDK, localharness, agent), on a Debian-based image with shell tooling for +# skills/code execution. Used by both the ax-server (`ax serve`) and the harness +# actor (`ax harness`, which forks the Python sidecar). +# +# Build context: repository root. The Antigravity runtime deps -- 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 build/deploy). +# +# 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 build --platform linux/amd64 \ +# --build-context wheels=$HOME/.cache/ax-antigravity-wheels \ +# -f cmd/ax/Dockerfile -t . && docker push + +# --- Stage 1: build the ax Go binary (with the harness build tag) ------------- +FROM --platform=$BUILDPLATFORM golang:1.26 AS build +ARG TARGETOS=linux +ARG TARGETARCH=amd64 +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -tags=harness -o /out/ax ./cmd/ax + +# --- Stage 2: comprehensive runtime (Debian + Python + Antigravity) ----------- +# Python image: a full Debian base (buildpack-deps) that +# already ships bash, git, curl, wget, ca-certificates, build-essential, and the +# common CLIs (find/ps/less/grep/sed/...). +FROM python:3.13 +WORKDIR /app + +# Antigravity runtime deps, installed offline from the pre-downloaded wheels. +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 + +# Python sidecar (with generated proto stubs), the agent it serves, and the ax +# binary built in stage 1. +COPY python/ /app/python +COPY examples/antigravity_agent/ /app/examples/antigravity_agent +COPY --from=build /out/ax /ax + +# `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 80 + +# Default command for local docker runs. Substrate ignores the image CMD and runs +# the ActorTemplate `command` instead. `ax harness` forks the Python sidecar, +# which serves the HarnessService on :80. +CMD ["/ax", "harness", \ + "--antigravity-agent-file", "/app/examples/antigravity_agent/agent.py", \ + "--host", "0.0.0.0", "--port", "80"] diff --git a/cmd/ax/harness.go b/cmd/ax/harness.go index 89a5858..ec69eae 100644 --- a/cmd/ax/harness.go +++ b/cmd/ax/harness.go @@ -12,30 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package main implements a demo HarnessService. -// It is intended for testing purposes only and should be replaced with a real -// implementation in production. -// TODO(wjjclaud): Replace this file with a real harness implementation. +// Package main: the `ax harness` command. It supervises the Antigravity Python +// sidecar server (which serves the HarnessService and gRPC health), forking it +// as a child process and forwarding termination signals. package main import ( "fmt" "log" - "net" "os" + "os/exec" "os/signal" - "sync" + "strconv" "syscall" - "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 ( - harnessPort int + harnessPort int + harnessHost string + harnessAntigravityAgentFile string ) var harnessCmd = &cobra.Command{ @@ -46,152 +43,42 @@ var harnessCmd = &cobra.Command{ } func init() { - harnessCmd.Flags().IntVar(&harnessPort, "port", 50053, "The port for the gRPC HarnessService to listen on") + harnessCmd.Flags().IntVar(&harnessPort, "port", 50053, "Port for the HarnessService to listen on") + harnessCmd.Flags().StringVar(&harnessHost, "host", "127.0.0.1", "Host interface for the HarnessService to bind") + harnessCmd.Flags().StringVar(&harnessAntigravityAgentFile, "antigravity-agent-file", "examples/antigravity_agent/agent.py", "Path to the agent config file the Python sidecar serves") rootCmd.AddCommand(harnessCmd) } +// runHarness forks the Antigravity Python sidecar server, which serves the +// HarnessService (and gRPC health) on the configured port. ax harness supervises +// the child: it forwards termination signals and exits with the child's status. func runHarness(cmd *cobra.Command, args []string) error { - lis, err := net.Listen("tcp", fmt.Sprintf(":%d", harnessPort)) - if err != nil { - return fmt.Errorf("failed to listen on port :%d: %w", harnessPort, err) + py := exec.Command("python3", "-m", "python.antigravity.harness_server", + "--host", harnessHost, + "--port", strconv.Itoa(harnessPort), + "--agent_file", harnessAntigravityAgentFile, + ) + py.Stdin = os.Stdin + py.Stdout = os.Stdout + py.Stderr = os.Stderr + py.Env = os.Environ() + + if err := py.Start(); err != nil { + return fmt.Errorf("failed to start antigravity harness server: %w", err) } + log.Printf("forked antigravity harness server (pid %d) on %s:%d", py.Process.Pid, harnessHost, harnessPort) - // Start gRPC Server - grpcServer := grpc.NewServer() - 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 + // Forward termination signals to the child so substrate can stop the actor. sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { - <-sigChan - log.Println("\nReceived shutdown signal, stopping gRPC HarnessService server gracefully...") - grpcServer.GracefulStop() + for sig := range sigChan { + _ = py.Process.Signal(sig) + } }() - log.Printf("gRPC HarnessService listening on port :%d...\n", harnessPort) - if err := grpcServer.Serve(lis); err != nil { - return fmt.Errorf("failed to serve gRPC: %w", err) + if err := py.Wait(); err != nil { + return fmt.Errorf("antigravity harness server exited: %w", err) } return nil } - -// conversationState is the per-conversation state the stub keeps in process memory. -// On substrate this state is preserved across turns by snapshot/suspend/resume. -type conversationState struct { - turns int - history []string -} - -// HarnessServiceServer implements the gRPC proto.HarnessServiceServer interface. -type HarnessServiceServer struct { - proto.UnimplementedHarnessServiceServer - - mu sync.Mutex - conversations map[string]*conversationState -} - -// NewHarnessServiceServer creates a new HarnessServiceServer. -func NewHarnessServiceServer() *HarnessServiceServer { - return &HarnessServiceServer{conversations: make(map[string]*conversationState)} -} - -// Connect implements one HarnessService turn. It reads the initial -// HarnessRequest{start} frame, then replies with a "hello world (turn N)" -// frame, an echo of each input, and a recap of the inputs from prior turns, -// terminating with HarnessEnd{STATE_COMPLETED}. -// -// The per-conversation turn count and input history are kept in process -// memory and persist across turns. -func (s *HarnessServiceServer) Connect(stream proto.HarnessService_ConnectServer) error { - req, err := stream.Recv() - if err != nil { - return err - } - - convID := req.GetConversationId() - if req.GetStart() == nil { - return stream.Send(&proto.HarnessResponse{ - ConversationId: convID, - Type: &proto.HarnessResponse_End{ - End: &proto.HarnessEnd{ - State: proto.State_STATE_FAILED, - ErrorMessage: "expected HarnessRequest{start} as the first frame", - }, - }, - }) - } - - // Collect this turn's input text(s). - var inputs []string - for _, m := range req.GetStart().GetMessages() { - if text := m.GetContent().GetText().GetText(); text != "" { - inputs = append(inputs, text) - } - } - - // Update per-conversation state held in process memory. - s.mu.Lock() - st := s.conversations[convID] - if st == nil { - st = &conversationState{} - s.conversations[convID] = st - } - st.turns++ - turn := st.turns - prior := "" - if len(st.history) > 0 { - prior = st.history[len(st.history)-1] - } - st.history = append(st.history, inputs...) - s.mu.Unlock() - - // Reply: turn number, this turn's inputs, and the remembered prior inputs. - if err := stream.Send(textOutput(convID, fmt.Sprintf("hello world (turn %d)", turn))); err != nil { - return err - } - for _, in := range inputs { - if err := stream.Send(textOutput(convID, "received: "+in)); err != nil { - return err - } - } - if prior != "" { - if err := stream.Send(textOutput(convID, "previously you said: "+prior)); err != nil { - return err - } - } - - return stream.Send(&proto.HarnessResponse{ - ConversationId: convID, - Type: &proto.HarnessResponse_End{ - End: &proto.HarnessEnd{State: proto.State_STATE_COMPLETED}, - }, - }) -} - -// textOutput builds a HarnessResponse carrying a single assistant text Message. -func textOutput(convID, text string) *proto.HarnessResponse { - return &proto.HarnessResponse{ - ConversationId: convID, - Type: &proto.HarnessResponse_Outputs{ - Outputs: &proto.HarnessOutputs{ - Messages: []*proto.Message{ - { - Role: "assistant", - Content: &proto.Content{ - Type: &proto.Content_Text{ - Text: &proto.TextContent{Text: text}, - }, - }, - }, - }, - }, - }, - } -} diff --git a/go.mod b/go.mod index 97c64ea..b35d8dd 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-20260616211440-0ec9b650a0ab + github.com/agent-substrate/substrate v0.0.0-20260617164646-00a90a8eda76 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 dc3da9e..dbe5823 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +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-20260616211440-0ec9b650a0ab h1:ESQ0AWzhw3GROJ5ZIT6tzqHg92mIEqdp/esSmK3s6zQ= -github.com/agent-substrate/substrate v0.0.0-20260616211440-0ec9b650a0ab/go.mod h1:TgdtEUV6iaflJTwmS8ONiGsyyJD+5okPZj2H6mM8WlA= +github.com/agent-substrate/substrate v0.0.0-20260617164646-00a90a8eda76 h1:GO7ajMplVYHUXAqT4yWTWKGMWsWehoL3TI5GXOwJSvo= +github.com/agent-substrate/substrate v0.0.0-20260617164646-00a90a8eda76/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= diff --git a/internal/hack/install-ax.sh b/internal/hack/install-ax.sh index 2ee316e..461693e 100755 --- a/internal/hack/install-ax.sh +++ b/internal/hack/install-ax.sh @@ -20,9 +20,9 @@ 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` +# Directory holding the pre-downloaded linux/amd64 wheels baked into the ax 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}" @@ -76,8 +76,8 @@ 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 " --fetch-wheels Download the antigravity wheels into WHEELS_DIR" + echo " --deploy-ax-server Build images and deploy AX server and components" echo " --delete-ax-server Delete AX server and components from cluster" echo " -h, --help Show this help message" } @@ -88,12 +88,6 @@ run_kubectl() { "$@" } -run_ko() { - GOFLAGS="-tags=harness" ko apply \ - ${KUBECTL_CONTEXT:+--context=${KUBECTL_CONTEXT}} \ - "$@" -} - # 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 @@ -144,10 +138,11 @@ fetch_wheels() { 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() { +# build_ax_image builds and pushes the comprehensive ax image (the Go ax binary +# plus the Antigravity Python sidecar) and echoes its digest-pinned reference on +# stdout. Requires KO_DOCKER_REPO, a container engine, and a populated wheel +# cache. +build_ax_image() { if [[ -z "${KO_DOCKER_REPO:-}" ]]; then echo "Error: KO_DOCKER_REPO environment variable must be set" >&2 exit 1 @@ -165,19 +160,31 @@ build_antigravity_image() { fi local repo tag image digest - repo="${KO_DOCKER_REPO}/ax-antigravity-harness" + repo="${KO_DOCKER_REPO}/ax" 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 + # Relabel multi-stage build step prefixes to friendlier tags matching log_step's + # style. + log_step "build_ax_image -> ${image}" >&2 "${CONTAINER_ENGINE}" build \ --platform linux/amd64 \ --build-context "wheels=${WHEELS_DIR}" \ - -f python/antigravity/Dockerfile \ + -f cmd/ax/Dockerfile \ -t "${image}" \ - . >&2 + . 2>&1 \ + | awk -v cyan="${COLOR_CYAN}" -v reset="${COLOR_RESET}" ' + /^\[[0-9]+\/[0-9]+\] / { + s = $1; gsub(/[][]/, "", s); split(s, parts, "/") + stage = (parts[1] == "1") ? "build" : "runtime" + rest = $0; sub(/^\[[0-9]+\/[0-9]+\] /, "", rest) + printf "%s[%s]%s %s\n", cyan, stage, reset, rest + fflush(); next + } + { print; fflush() } + ' >&2 # Push the readable tag, then resolve the pushed manifest digest so the # ActorTemplate can reference the image by digest (snapshot-safe). @@ -246,18 +253,18 @@ deploy_ax_server() { echo "Using GCS Bucket: ${BUCKET_NAME}" - # Build and push the antigravity harness image, capturing its reference. - local antigravity_image ateom_image - antigravity_image=$(build_antigravity_image) + # Build and push the images, capturing their digest-pinned references. + local ax_image ateom_image + ax_image=$(build_ax_image) ateom_image=$(build_ateom_image) - # Render template and apply with ko + # Render the manifest and apply it. sed -e "s|\${GEMINI_API_KEY}|${GEMINI_API_KEY}|g" \ -e "s|\${BUCKET_NAME}|${BUCKET_NAME}|g" \ - -e "s|\${ANTIGRAVITY_IMAGE}|${antigravity_image}|g" \ + -e "s|\${AX_IMAGE}|${ax_image}|g" \ -e "s|\${ATEOM_IMAGE}|${ateom_image}|g" \ internal/manifests/ax-deployment2.yaml \ - | run_ko -f - + | run_kubectl apply -f - # Wait for the antigravity ActorTemplate's golden snapshot to be ready. log_step "wait for actortemplate/ax-harness-template to be Ready" @@ -272,7 +279,7 @@ delete_ax_server() { # 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" \ + -e "s|\${AX_IMAGE}|dummy-image|g" \ -e "s|\${ATEOM_IMAGE}|dummy-image|g" \ internal/manifests/ax-deployment2.yaml \ | run_kubectl delete --ignore-not-found -f - diff --git a/internal/manifests/README.md b/internal/manifests/README.md index f4d8cf1..b00cb6e 100644 --- a/internal/manifests/README.md +++ b/internal/manifests/README.md @@ -27,20 +27,21 @@ and container image are provided by AX. ### 1. Build and Deploy > [!NOTE] -> 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}` and `${ATEOM_IMAGE}` references 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 `${AX_IMAGE}` and `${ATEOM_IMAGE}` references during deployment. The installation script builds the required images and applies the resolved manifests to your cluster: -- the AX control-plane (Go) image, built with `ko` using the `harness` build tag; -- the built-in **antigravity harness** image, built from - `python/antigravity/Dockerfile` with Docker or Podman; +- the comprehensive **ax** image, built from `cmd/ax/Dockerfile` with Docker or + Podman (the `harness`-tagged Go `ax` binary plus the Antigravity Python sidecar + on a Debian base). The ax-server runs `ax serve`; the harness actor runs + `ax harness`, which forks the sidecar; - the **ateom-gvisor** worker image, built with `ko` from the `go.mod` pinned substrate module. #### Build prerequisites -The antigravity image bundles the antigravity SDK and its `localharness` binary, +The ax 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): @@ -55,7 +56,7 @@ installed offline from a pre-downloaded linux/amd64 wheel cache. Fetch it once > (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 +You also need a container engine to build and push the ax 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`: diff --git a/internal/manifests/ax-deployment2.yaml b/internal/manifests/ax-deployment2.yaml index d33dd06..231da8e 100644 --- a/internal/manifests/ax-deployment2.yaml +++ b/internal/manifests/ax-deployment2.yaml @@ -45,22 +45,15 @@ spec: workerPoolRef: name: ax-harness-workerpool namespace: ax - runsc: - amd64: - url: "gs://gvisor/releases/nightly/2026-05-19/x86_64/runsc" - sha256Hash: "a397be1abc2420d26bce6c70e6e2ff96c73aaaab929756c56f5e2089ea842b63" - arm64: - url: "gs://gvisor/releases/nightly/2026-05-19/aarch64/runsc" - sha256Hash: "1ba2366ae2efceba166046f51a4104f9261c9cb72c6db8f5b3fe2dc57dea86b9" pauseImage: "gcr.io/gke-release/pause@sha256:bcbd57ba5653580ec647b16d8163cdd1112df3609129b01f912a8032e48265da" containers: - name: "axharness" - image: ${ANTIGRAVITY_IMAGE} + image: ${AX_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", + # environment are specified here. `ax harness` forks the Antigravity Python + # sidecar, which serves on port 80 because substrate DNATs workerPodIP:80. + command: ["/ax", "harness", + "--antigravity-agent-file", "/app/examples/antigravity_agent/agent.py", "--host", "0.0.0.0", "--port", "80"] env: - name: GEMINI_API_KEY @@ -95,8 +88,8 @@ spec: spec: containers: - name: ax-server - image: ko://github.com/google/ax/cmd/ax - command: ["/ko-app/ax", "serve", "--config", "/etc/ax/ax.yaml"] + image: ${AX_IMAGE} + command: ["/ax", "serve", "--config", "/etc/ax/ax.yaml"] ports: - containerPort: 8494 env: diff --git a/python/antigravity/Dockerfile b/python/antigravity/Dockerfile deleted file mode 100644 index bdb7341..0000000 --- a/python/antigravity/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -# 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 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"]