Repository navigation
feat(router): ship the Go router image and remove the Python implementation - #10359
Conversation
Build the router image from the Go binary on scratch and switch AWF's router healthcheck to the binary's exec-form `healthcheck` subcommand. Bump the router to 0.2.0 and drop the 504 request_timeout from openapi.yaml, since net/http closes connections at its deadlines. The builder uses the Go 1.27.3 pin and filters the image's CA bundle through AWF's CA distrust policy, as the Python image did. Port the remaining Python-only tests to Go: the container tests, the routing-contract corpus replay, and checks of openapi.yaml, the table schema and documentation links. Fold the known Go differences into the parity fixtures, which are now edited by hand, and delete the Python package, tests, lockfiles, Unicode data, CI jobs and Dependabot entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The go/ directory only existed to sit beside the Python package's src/. With Python gone, the router follows Go's layout for a single command: go.mod and the main package's files at the module root, beside the Dockerfile, as the API proxy's sources sit beside theirs. Test fixtures move from tests/fixtures/ to testdata/, which the go tool ignores. The module path now matches its directory. The tests read the tables, schemas, examples, fixtures and openapi.yaml relative to the package instead of through "..". The Dockerfile, .dockerignore allowlist, .gitignore, test-router.yml, README and release docs follow the new paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Port the remaining Python checks: the README's examples run against the bundled tables and match its classifier output and route response examples, every openapi.yaml $ref resolves, and the routing-contract corpus has unique IDs, a case for every endpoint and explicit-* cases for all six profiles. - Fail on corpus files that are neither case lists nor a response_file, instead of skipping them. - Check that the container exits 0 within the 1 s that AWF's teardown (docker compose down -t 1) allows, rather than timing docker stop. - Signal the shutdown tests' server once it holds the request, instead of after a fixed sleep, which failed under load. - Describe behavior in comments without the removed Python service, except where it explains a rule or where expected values came from, and say that the container tests also make the root filesystem read-only, which AWF doesn't. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Documentation PreviewDocumentation has been built for this PR. To view locally:
Built from commit 4b7f6ed |
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (1 files)
Coverage comparison generated by |
There was a problem hiding this comment.
🟡 Changes recommended
Literal VERSION validation and contract-table schema coverage must be fixed before approval.
1 open issue
What changed in this PR
Replaces AWF’s Python router with Go 0.2.0, shipping a static binary on scratch.
Changes:
- Switches the router image and AWF healthcheck to Go.
- Relocates Go sources and fixtures; removes Python implementation and tooling.
- Adds replacement tests and updates CI, release configuration, and OpenAPI.
| File | Description |
|---|---|
tests/integration/model-routing.test.ts |
Builds the local router unless an image is supplied. |
src/services/router-service.ts |
Runs the Go healthcheck subcommand. |
src/compose-generator.test.ts |
Expects the Go healthcheck command. |
docs/releasing.md |
Documents Go version verification. |
containers/router/version_test.go |
Checks version declarations in the relocated module. |
containers/router/tests/test_service.py |
Removes Python service tests. |
containers/router/tests/test_parity_corpus.py |
Removes Python corpus replay. |
containers/router/tests/test_heuristics.py |
Removes Python heuristic tests. |
containers/router/tests/test_examples.py |
Replaces Python example checks with Go coverage. |
containers/router/tests/test_contracts.py |
Removes Python contract tests. |
containers/router/tests/test_classification.py |
Removes Python classification tests. |
containers/router/tests/fixtures/parity/unicode/whitespace.json |
Removes Python Unicode reference data. |
containers/router/tests/contract_corpus.py |
Removes the contract archive exporter. |
containers/router/tests/conftest.py |
Removes Python test setup. |
containers/router/testdata/routing-contract/errors.json |
Relocates error contract cases. |
containers/router/testdata/routing-contract/discovery.json |
Updates the expected router version. |
containers/router/testdata/routing-contract/classify.json |
Relocates classification contract cases. |
containers/router/testdata/routing-contract/classify-response.json |
Relocates the classifier response fixture. |
containers/router/testdata/parity/labels.json |
Records Go Unicode behavior. |
containers/router/testdata/parity/context.json |
Removes unsupported nonfinite-number cases. |
containers/router/table.go |
Relocates routing-table loading and validation. |
containers/router/table_test.go |
Updates table paths and comments. |
containers/router/src/gh_aw_router/service.py |
Removes Python service composition. |
containers/router/src/gh_aw_router/routing.py |
Removes Python context estimation. |
containers/router/src/gh_aw_router/routing_table.py |
Removes Python table routing. |
containers/router/src/gh_aw_router/py.typed |
Removes the Python typing marker. |
containers/router/src/gh_aw_router/contracts.py |
Removes Python contract models. |
containers/router/src/gh_aw_router/cli.py |
Removes the Python CLI. |
containers/router/src/gh_aw_router/classification.py |
Removes Python classification. |
containers/router/src/gh_aw_router/__main__.py |
Removes the Python entrypoint. |
containers/router/src/gh_aw_router/__init__.py |
Removes Python package metadata. |
containers/router/server.go |
Relocates the server and updates commentary. |
containers/router/server_test.go |
Shares route expectations and updates comments. |
containers/router/serve.go |
Relocates lifecycle and healthcheck logic. |
containers/router/serve_test.go |
Synchronizes shutdown signals with active requests. |
containers/router/schema_test.go |
Adds schema checks; contract tables are overwritten and skipped before validation. |
containers/router/route_test.go |
Moves corpus replay into dedicated tests. |
containers/router/requirements.lock |
Removes Python runtime dependency pins. |
containers/router/pyproject.toml |
Removes Python packaging configuration. |
containers/router/parity_test.go |
Uses relocated, consolidated Go expectations. |
containers/router/openapi.yaml |
Updates version and removes the 504 response. |
containers/router/openapi_test.go |
Adds API checks; optional correction retains every documented method per path. |
containers/router/main.go |
Bumps the router version to 0.2.0. |
containers/router/logging.go |
Clarifies historical log-level commentary. |
containers/router/logging_test.go |
Relocates structured-logging tests. |
containers/router/heuristics.go |
Clarifies Unicode behavior comments. |
containers/router/heuristics_test.go |
Uses committed Go expectations directly. |
containers/router/go/go.mod |
Removes the old module location. |
containers/router/go.mod |
Defines the relocated module and toolchain. |
containers/router/docs_test.go |
Checks documentation examples and links. |
containers/router/Dockerfile |
Builds the static Go image; VERSION validation incorrectly accepts regex matches. |
containers/router/decode.go |
Updates decoding comments. |
containers/router/decode_test.go |
Updates example fixture paths. |
containers/router/corpus_test.go |
Adds HTTP corpus replay and coverage checks. |
containers/router/contract.go |
Removes the timeout error constant. |
containers/router/container_test.go |
Adds hardened-image tests; optional correction makes table files readable under restrictive umasks. |
containers/router/cli.go |
Clarifies validation-order commentary. |
containers/router/cli_test.go |
Updates paths and shutdown synchronization. |
containers/router/classify.go |
Relocates classification implementation. |
containers/router/classify_test.go |
Relocates classification coverage. |
containers/router/.gitignore |
Replaces Python exclusions with binary exclusions. |
containers/router/.dockerignore |
Allowlists Go image build inputs. |
containers/api-proxy/fixtures/routing-corpus.js |
Updates fixture-refresh guidance. |
.github/workflows/test-router.yml |
Replaces Python jobs with Go and container checks. |
.github/workflows/release.yml |
Removes router QEMU setup. |
.github/dependabot.yml |
Removes Python dependency tracking and tracks the Go builder. |
Review effort: Max
- The Dockerfile matches VERSION as a fixed string, so a regex such as 0x2x0 no longer passes the version check. - The schema test keys tables by source, so the bundled tables no longer replace the contract tables, and it checks that all of them are covered. - The OpenAPI test keeps every documented method per path. - The container corpus test sets the table files' modes with chmod, so a restrictive umask can't make them unreadable to the container user. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
✅ Copilot review passed with no inline comments. @rabeckett Add the |
|
Thanks for the review. e2dee29 also fixes the three points that appeared only in the review summary:
|
|
📡 Smoke OTel Tracing completed. All tracing scenarios validated. ✅
|
|
Smoke Cloud Hypervisor completed. Cloud Hypervisor + Copilot passed.
|
|
✅ Build Test Suite completed successfully!
|
|
🔌 Smoke Services — All services reachable! ✅
|
|
✅ Smoke Copilot BYOK completed. Copilot BYOK mode operational. 🔓
|
|
✅ Smoke Claude passed
|
|
✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟
|
|
📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤
|
|
✅ Smoke Gemini completed. All facets verified. 💎
|
|
✅ Smoke Copilot BYOK AOAI (api-key) completed. Copilot AOAI BYOK (api-key) mode operational. 🔓
|
|
🚀 Security Guard has started processing this pull request |
|
Chroot tests passed! Smoke Chroot - All security and functionality tests succeeded.
|
|
✅ Smoke Copilot BYOK AOAI (Entra) completed. Copilot AOAI BYOK (Entra) mode operational. 🔓
|
|
🛡️ Smoke Copilot Network Isolation confirmed the egress allowlist is enforced. ✅
|
|
Cloud Hypervisor + Copilot smoke test
|
|
Smoke Copilot: PR "fix: repair Docker enclave smoke workflow compatibility"
|
Smoke Test: Claude Engine Validation
Overall result: PASS
|
|
Services smoke test: ✅ Redis PONG, ✅ pg_isready accepting connections, ✅ SELECT 1 = 1. PASS
|
|
EGRESS_RESULT allow=pass deny=pass
Overall: PASS — cc @rabeckett
|
Copilot BYOK (Direct Mode) Smoke Test✅ GitHub MCP connectivity — Verified via API Overall: PASS Running in direct BYOK mode (
|
|
@rabeckett Tests: MCP API ✓ GitHub Connectivity ✓ File I/O ✓ BYOK inference ✓
|
|
Smoke OTEL tracing
|
Chroot Version Comparison
Node.js differs between host and chroot, so the tests did not all pass and the
|
🏗️ Build Test Suite Results
Overall: 5/8 ecosystems passed — FAIL All 8 clones succeeded. Failures
The
|
Smoke Test
|
Smoke Test Results
|
|
fix: use Responses for Azure o4-mini smoke requests: ✅
|

Closes #10358
Part of #10261 (step 5 of 7).
Summary
This PR makes the Go port the router:
scratch;healthchecksubcommand instead of Python;containers/router/go/tocontainers/router/;The HTTP API, the CLI, the image name, the port, the environment variables and the tables path stay the same. The only API change is that
openapi.yamlno longer documents the 504request_timeoutresponse (see the decisions below).Changes
Dockerfile,.dockerignore):golang:1.27.3-alpine, pinned by digest atgo.mod's toolchain version. It runs on$BUILDPLATFORMand cross-compiles a static binary (CGO_ENABLED=0,-trimpath), so the release's two-platform build needs no emulation.FROM scratch. It holds only the binary at/usr/local/bin/gh-aw-router, the six tables in/routing,/app/openapi.yaml, and a CA bundle for HTTPS OTLP export later. The builder filters that bundle through AWF's CA distrust policy intrust/, as the Python image did. Directories and the binary are 0555; data files are 0444.OPENAPI_SHA256matchesopenapi.yaml, and unless the optionalVERSIONargument matchesversioninmain.go.OPENAPI_SHA256on its own is still enough for a build, so AWF's--build-localandsupply-chain-scan.ymlare unchanged.USER 10001:10001,GH_AW_ROUTER_ROUTING_TABLES=/routing,EXPOSE 8737,STOPSIGNAL SIGTERMand theservecommand.GOMAXPROCS=2andGOMEMLIMIT=384MiB, because AWF limits the router to 512 MiB and 100 processes but sets no CPU quota.HEALTHCHECK, like the other AWF images..dockerignoreis an allowlist: theDockerfile,openapi.yaml,go.mod, the non-test Go files,routing/cost-*.jsonandtrust/.src/services/router-service.ts:ROUTER_HEALTHCHECK_TESTbecomes the exec-formCMD /usr/local/bin/gh-aw-router healthcheck, with the same timings. That command needs no tables and never uses a proxy, so theROUTER_PORTconstant is gone.src/compose-generator.test.tsexpects the new command.tests/integration/model-routing.test.tsused to defaultROUTER_IMAGEto the legacy Python image (ghcr.io/githubnext/gh-aw-router). WithoutROUTER_IMAGE, its compose file now buildscontainers/router, so it can't pair the Go healthcheck with a Python image.containers/api-proxy/fixtures/routing-corpus.js: a comment now says to refresh cases fromtestdata/routing-contract, since the contract exporter is gone.go/*.goandgo.modmove up tocontainers/router/. They form one flatmainpackage next toopenapi.yaml,routing/,schemas/,examples/andtrust/.github.com/github/gh-aw-firewall/containers/router.tests/fixtures/becomestestdata/.container_test.goreplacestest_container.pyand runs whenGH_AW_ROUTER_TEST_IMAGEnames a built image.HEALTHCHECK, and labels.healthcheck.docker compose down -t 1) allows.corpus_test.goreplaces the corpus replay intest_http.py.explicit-*cases cover all six profiles.openapi_test.goreplacestest_openapi.py. It checks thatopenapi.yamldocuments exactly the served paths, methods and error codes, and that every$refresolves.schema_test.goreplacestest_table_schema.py. It pins the published table schema's hash, checks its enums and patterns against the Go types, and checks the bundled and contract tables against those patterns.docs_test.goreplacestest_examples.pyand the documentation link check.curlexamples to a server with the bundled tables.unicode/is deleted.src/gh_aw_router/;differential.pyand the contract exporter;pyproject.toml,uv.lockandrequirements.lock;.gitignoreentries.test-router.yml:checks,advisories(pip-audit) anddifferentialjobs;containers/router, still on Linux and Windows, with-raceandgovulncheckon Linux;containerjob that builds the image and runs the container tests.release.ymlbuild-routerdrops the QEMU step. Its build arguments and label checks are unchanged.dependabot.ymldrops the router'suventry and itspythonignore. The router's Docker entry now tracks thegolangbuilder. There's nogomodentry, because the module has no dependencies.healthcheck, serving limits, log format, container and tests.docs/releasing.mdsays that the build checksVERSIONagainstmain.go.Decisions for maintainers
HEALTHCHECK.net/httpserver closes a connection that passes its read or write deadline instead of answering it, soopenapi.yamldrops the 504request_timeoutresponse.routing-router-client.js) marks both a 5xx and a dropped connection as transient, so it handles the two the same way./capabilities, not its version, so it needs no change.jsonschemavalidation of corpus requests and example responses againstopenapi.yaml's schemas, because Go's standard library has no JSON Schema validator. The corpus replay pins the runtime behavior. As the README asks, review keeps the OpenAPI schemas in step with it.Validation
gofmt -l,go vetandgo test ./...pass incontainers/router, with Go 1.27.2 on Windows and in agolang:1.27.3Linux container.TestServeShutdownpassed 30 runs in a row on Windows and 20 on Linux.docker buildx build --platform linux/amd64,linux/arm64withbuild-router'sVCS_REFandOPENAPI_SHA256, on a builder with only nativelinux/arm64and no QEMU.OPENAPI_SHA256fails at the checksum step, and a mismatchedVERSIONfails at the version check.src/compose-generator.test.tsandscripts/security/ca-trust.test.tspass (Jest on Linux, in Docker).tests/integration/model-routing.test.tsas is, because itscleanup()prunes every stopped container and unused network on the host.docker stop -t 1..grype.yamlfinds no vulnerabilities in the image. This PR's supply-chain scan rebuilds the image and runs the same blocking gate.test-model-routingruns only onmain, so this PR gets no routed end-to-end run in CI. It will run on the merge commit.firewall.model_routingrecords with a run on the previous release. gh-aw should bump its default AWF version only after that passes.Rollback
AWF and its router image ship together, so roll back both together: