diff --git a/platform/cluster/flux/apps/agents/agents-login/worker.yaml b/platform/cluster/flux/apps/agents/agents-login/worker.yaml index a7a5b7ef..9452044b 100644 --- a/platform/cluster/flux/apps/agents/agents-login/worker.yaml +++ b/platform/cluster/flux/apps/agents/agents-login/worker.yaml @@ -45,6 +45,73 @@ spec: fsGroup: 1000 seccompProfile: type: RuntimeDefault + # Codex 0.141.x ships a musl static binary. In this cluster IPv6 egress is + # unreachable, and musl ignores /etc/gai.conf, so auth.openai.com AAAA + # answers can make `codex login --device-auth` fail before it tries IPv4. + # Resolve A records at pod startup and mount a pod-local hosts file instead + # of using static hostAliases, which would stale when Cloudflare rotates IPs. + initContainers: + - name: codex-auth-hosts + image: ghcr.io/extratoast/personal-stack/agents-login:latest + imagePullPolicy: Always + command: + - node + - -e + - | + const dns = require('node:dns').promises; + const { writeFileSync } = require('node:fs'); + + const hosts = (process.env.CODEX_IPV4_HOSTS || 'auth.openai.com') + .split(/[\s,]+/) + .filter(Boolean); + const podIp = process.env.POD_IP || '127.0.0.1'; + const hostname = process.env.HOSTNAME || 'agents-login-worker'; + const lines = [ + '# Kubernetes hosts file generated by codex-auth-hosts.', + '127.0.0.1\tlocalhost', + '::1\tlocalhost ip6-localhost ip6-loopback', + 'fe00::0\tip6-localnet', + 'fe00::0\tip6-mcastprefix', + 'fe00::1\tip6-allnodes', + 'fe00::2\tip6-allrouters', + `${podIp}\t${hostname}`, + '', + ]; + + (async () => { + for (const host of hosts) { + const ips = await dns.resolve4(host); + if (ips.length === 0) { + throw new Error(`no A records for ${host}`); + } + for (const ip of ips) { + lines.push(`${ip}\t${host}`); + } + console.log(`resolved ${host}: ${ips.join(', ')}`); + } + writeFileSync('/generated-hosts/hosts', `${lines.join('\n')}\n`, { mode: 0o644 }); + })().catch((err) => { + console.error(err); + process.exit(1); + }); + env: + - name: CODEX_IPV4_HOSTS + value: auth.openai.com + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + securityContext: + runAsNonRoot: false + runAsUser: 0 + runAsGroup: 0 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ['ALL'] + volumeMounts: + - name: codex-auth-hosts + mountPath: /generated-hosts containers: - name: worker image: ghcr.io/extratoast/personal-stack/agents-login:latest @@ -84,6 +151,10 @@ spec: capabilities: drop: ['ALL'] volumeMounts: + - name: codex-auth-hosts + mountPath: /etc/hosts + subPath: hosts + readOnly: true - name: home mountPath: /home/agent - name: tmp @@ -106,6 +177,8 @@ spec: cpu: 500m memory: 512Mi volumes: + - name: codex-auth-hosts + emptyDir: {} - name: home emptyDir: {} - name: tmp diff --git a/services/agents-login/Dockerfile b/services/agents-login/Dockerfile index 86cae227..023c1019 100644 --- a/services/agents-login/Dockerfile +++ b/services/agents-login/Dockerfile @@ -33,12 +33,6 @@ RUN npm install -g \ @openai/codex@${CODEX_VERSION} \ && npm cache clean --force -# Prefer IPv4: the cluster has no working IPv6 egress, but external hosts (e.g. -# auth.openai.com) publish AAAA records. glibc getaddrinfo (used by codex's Rust -# HTTP client) otherwise returns IPv6 first and codex fails with ENETUNREACH / -# "error sending request". Ranking IPv4-mapped addresses first makes codex use IPv4. -RUN printf 'precedence ::ffff:0:0/96 100\n' > /etc/gai.conf - ENV NODE_ENV=production \ HOME=/home/agent \ CODEX_HOME=/home/agent/.codex