diff --git a/.downstream/native-domains.md b/.downstream/native-domains.md index 51f07c60b868..0f3fa818eae8 100644 --- a/.downstream/native-domains.md +++ b/.downstream/native-domains.md @@ -96,7 +96,11 @@ groups, quota or scheduling state. Normal credential refresh remains available. Source acquisition shares one process-local `__oailb` candidate. Its maximum lease is 230 seconds, capped by an earlier upstream expiry; receiving the same value during a live lease does not extend it. Source preparation is sequential -and has a 90-second budget. Each target/model is validated with two completed +and uses the ordinary OAuth account-test payload (default instructions, fixed +short user prompt, medium effort), with its own configured normal connection +policy. It does not inherit target-probe session/window headers, routing hints, +encrypted-output options or connection-close behavior. +It has a 90-second budget. Each target/model is validated with two completed HTTP 200 streams, at most 45 seconds each: the first must return STATE, and the second may omit STATE or return the same value. Qualification includes the target identity, actual model, exit, client headers, STATE and TLS profile. diff --git a/.github/buildkitd.toml b/.github/buildkitd.toml new file mode 100644 index 000000000000..cafe35af5927 --- /dev/null +++ b/.github/buildkitd.toml @@ -0,0 +1,4 @@ +# Google-operated cache of public Docker Hub images. BuildKit keeps the original +# image references and validates their content digests; no credentials change. +[registry."docker.io"] + mirrors = ["mirror.gcr.io"] diff --git a/.github/scripts/prepare_test_images.py b/.github/scripts/prepare_test_images.py new file mode 100644 index 000000000000..a97cd742967d --- /dev/null +++ b/.github/scripts/prepare_test_images.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Preload unchanged CI image names from their publishers' alternate registries. + +The OCI index digests below were checked against the matching Docker Hub tags. +Postgres/Redis are Docker Official Images on ECR Public; Ryuk is published by +Testcontainers on GHCR. No Docker Hub credentials or pull-limit workarounds are +used. Docker verifies immutable digests; existing tests use these local aliases. +""" + +import subprocess + + +IMAGES = ( + ("public.ecr.aws/docker/library/postgres@sha256:aa6eb304ddb6dd26df23d05db4e5cb05af8951cda3e0dc57731b771e0ef4ab29", "postgres:18.1-alpine3.23"), + ("public.ecr.aws/docker/library/redis@sha256:0514fa59e3d84e2f3be66731c5401fc2a85b8ea71b49c0116c850f6a558076f4", "redis:8.4-alpine"), + ("ghcr.io/testcontainers/ryuk@sha256:31b31269d06603366cbfd0284708dcd2e281e8a4188e53fce3d3304439d0df3d", "testcontainers/ryuk:0.13.0"), +) + + +def prepare(run=subprocess.run): + for source, local_name in IMAGES: + run(["docker", "pull", "--platform", "linux/amd64", source], check=True) + run(["docker", "tag", source, local_name], check=True) + + +if __name__ == "__main__": + prepare() diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index bf8a2ba4579f..4f9dbdcd374f 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -45,7 +45,9 @@ jobs: run: make test-unit - name: Integration tests working-directory: backend - run: make test-integration + run: | + python3 ../.github/scripts/prepare_test_images.py + make test-integration frontend: runs-on: ubuntu-latest diff --git a/.github/workflows/downstream-release.yml b/.github/workflows/downstream-release.yml index 76ad8be90980..6c2e763b2eca 100644 --- a/.github/workflows/downstream-release.yml +++ b/.github/workflows/downstream-release.yml @@ -94,6 +94,9 @@ jobs: python3 .github/scripts/release_preflight.py --stage before-build \ --tag "$RELEASE_TAG" --source-sha "$SOURCE_SHA" - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + with: + driver-opts: image=mirror.gcr.io/moby/buildkit@sha256:cec9f139f45e93c5c69c60f8b07cfad9f43f4ef6b6a6cd917527fea5ff2e3dea + buildkitd-config: .github/buildkitd.toml - name: Build release image without publication id: build uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 diff --git a/.github/workflows/downstream-verify.yml b/.github/workflows/downstream-verify.yml index a6246bc3525d..2a0d1d1d4ac1 100644 --- a/.github/workflows/downstream-verify.yml +++ b/.github/workflows/downstream-verify.yml @@ -45,6 +45,7 @@ jobs: - name: Native gateway borrowing contracts working-directory: backend run: | + python3 ../.github/scripts/prepare_test_images.py go test -p=1 -tags unit ./internal/service ./internal/repository ./internal/handler ./internal/handler/admin -run '^Test(CodexGatewayBorrow|Pelican|AccountObservation|ObservedAccountLease)' -count=1 go test -p=1 -tags integration ./internal/repository -run '^TestCodexGatewayBorrowTestRepositoryIntegrationLifecycle$' -count=1 - name: Codex preparation coordination race tests @@ -194,6 +195,9 @@ jobs: steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + with: + driver-opts: image=mirror.gcr.io/moby/buildkit@sha256:cec9f139f45e93c5c69c60f8b07cfad9f43f4ef6b6a6cd917527fea5ff2e3dea + buildkitd-config: .github/buildkitd.toml - name: Build candidate uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: diff --git a/backend/internal/service/codex_gateway_borrow_probe.go b/backend/internal/service/codex_gateway_borrow_probe.go index 5c3c1ff870a0..6c9a5a1d2c25 100644 --- a/backend/internal/service/codex_gateway_borrow_probe.go +++ b/backend/internal/service/codex_gateway_borrow_probe.go @@ -105,8 +105,11 @@ func (s *CodexGatewayBorrowService) acquireSource(ctx context.Context, id int64) if err != nil { return nil, err.Error(), err } - shot, err := s.fireObservation(ctx, template.Header, a, codexGatewayBorrowSourceModel, "Reply with OK only.", "medium", proxy, nil, - "", nil, HTTPUpstreamProfileCodexBorrowSource, codexGatewayBorrowProbeMaxBody) + req, err := borrowSourceObservationRequest(ctx, template.Header) + if err != nil { + return nil, err.Error(), err + } + shot, err := s.sendObservation(req, a, proxy, nil, codexGatewayBorrowSourceModel, codexGatewayBorrowProbeMaxBody) if err != nil { return nil, shot.errorText, err } @@ -125,6 +128,36 @@ func (s *CodexGatewayBorrowService) acquireSource(ctx context.Context, id int64) return candidate, "", nil } +// The pinned ranxi source acquisition uses the ordinary OAuth account-test +// payload, not the target's two-shot STATE-probe protocol. Preserve the shared +// account identity and observation isolation, but do not manufacture probe +// sessions, a window or a routing hint, or force this normal source connection +// closed. The source keeps its own configured OpenAI transport and proxy. +func borrowSourceObservationRequest(ctx context.Context, headers http.Header) (*http.Request, error) { + ctx = WithCodexGatewayBorrowObservation(WithHTTPUpstreamRedirectsDisabled(WithHTTPUpstreamProfile(ctx, HTTPUpstreamProfileCodexBorrowSource))) + payload := createOpenAITestPayload(codexGatewayBorrowSourceModel, true, "Reply with OK only.") + payload["reasoning"] = map[string]string{"effort": "medium"} + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, chatgptCodexURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Host, req.Header = "chatgpt.com", headers.Clone() + if req.Header == nil { + req.Header = make(http.Header) + } + for _, name := range []string{"Cookie", "Content-Length", "Content-Encoding", "Content-MD5", "X-Codex-Turn-State", "X-Codex-Turn-Metadata", "X-OpenAI-Internal-Codex-Responses-Lite", "conversation_id", "session_id", "session-id", "thread-id", "x-client-request-id", "x-codex-window-id", openAICodexRoutingHintHeader} { + deleteOpenAIHeaderEqualFold(req.Header, name) + } + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("OpenAI-Beta", "responses=experimental") + return req, nil +} + func borrowCookieScoped(cookie *http.Cookie, path string) bool { if cookie == nil || (cookie.Domain != "" && !strings.EqualFold(strings.TrimPrefix(cookie.Domain, "."), "chatgpt.com")) { return false diff --git a/backend/internal/service/codex_gateway_borrow_source_contract_test.go b/backend/internal/service/codex_gateway_borrow_source_contract_test.go new file mode 100644 index 000000000000..2dffdb990340 --- /dev/null +++ b/backend/internal/service/codex_gateway_borrow_source_contract_test.go @@ -0,0 +1,37 @@ +package service + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestCodexBorrowSourceUsesNormalAccountTestContract(t *testing.T) { + headers := http.Header{"Authorization": {"Bearer synthetic-source"}, "User-Agent": {"synthetic-persisted-identity"}, "Originator": {"codex-tui"}, "Version": {"0.162.1"}} + for _, name := range []string{"session-id", "thread-id", "session_id", "x-codex-window-id", "x-codex-turn-state", "x-codex-turn-metadata", "x-codex-routing-hint", "Cookie", "Content-Encoding"} { + headers.Set(name, "target-only-value") + } + original := headers.Clone() + req, err := borrowSourceObservationRequest(context.Background(), headers) + require.NoError(t, err) + require.False(t, req.Close, "source acquisition keeps normal connection policy") + require.Equal(t, original, headers) + require.Equal(t, "Bearer synthetic-source", req.Header.Get("Authorization")) + require.Equal(t, "synthetic-persisted-identity", req.Header.Get("User-Agent")) + for _, name := range []string{"session-id", "thread-id", "session_id", "x-codex-window-id", "x-codex-turn-state", "x-codex-turn-metadata", "x-codex-routing-hint", "Cookie", "Content-Encoding"} { + require.Empty(t, req.Header.Get(name), name) + } + body := borrowCoreBody(t, req) + want := createOpenAITestPayload("gpt-6-astra", true, "Reply with OK only.") + require.Equal(t, want["instructions"], gjson.GetBytes(body, "instructions").String()) + require.Equal(t, "Reply with OK only.", gjson.GetBytes(body, "input.0.content.0.text").String()) + require.Equal(t, "medium", gjson.GetBytes(body, "reasoning.effort").String()) + require.False(t, gjson.GetBytes(body, "parallel_tool_calls").Exists()) + require.False(t, gjson.GetBytes(body, "include").Exists()) + require.Equal(t, HTTPUpstreamProfileCodexBorrowSource, HTTPUpstreamProfileFromContext(req.Context())) + require.True(t, IsCodexGatewayBorrowObservation(req.Context())) + require.True(t, HTTPUpstreamRedirectsDisabled(req.Context())) +}