# Foundation agent container for Bedrock AgentCore Runtime.
#
# AgentCore contract: an HTTP server on :8080 exposing POST /invocations and
# GET /ping. Runs as a non-root user; scratch space is the session microVM's
# ephemeral filesystem (dies with the session).
#
# Build from the FOUNDATION ROOT:
#   docker build -f packages/infra/agent-image/Dockerfile .
#
# Pins (binding):
#   - Bun 1.4.0   (omp's engines floor is `>=1.3.14`; pinned exact to the
#     version that WRITES this repo's bun.lock — 1.3.14 cannot read a
#     lockfileVersion 2 lockfile and fails the install below outright)
#   - omp @oh-my-pi/pi-coding-agent@18.1.14 (all @oh-my-pi/pi-*@18.1.14 in
#     lockstep; the EXACT-pin shape is enforced below against
#     packages/agent/package.json, and the frozen lockfile pins the version itself)
#   - Python 3.10+ for the omp `eval` tool (the bun base is trixie: 3.13)
#   - gh 2.97.0, built from source with Go (see the gh-build stage)
#
# Secrets bootstrap: the container receives ONE pointer env var,
# RUNTIME_SECRET_ID. At start, server.ts fetches that JSON secret from Secrets
# Manager, setdefaults each key into process env (explicit env wins), and logs
# ONLY the number of keys loaded — never key names or values.

# gh version is shared by the builder stage and the runtime stage.
# 2.71.0+ required: older gh hard-fails `gh pr view/create` under App
# installation tokens after GitHub removed classic-Projects projectCards
# (cli/cli#10714) — the error text mimics a permissions failure.
ARG GH_VERSION=2.97.0

# GLOBAL build args: an ARG consumed by a `FROM` must be declared BEFORE the
# first stage. Declared after one, it belongs to that stage's scope and
# resolves to the empty string ("base name should not be blank").
#
# BUN_IMAGE defaults to Docker Hub so a plain local `docker build` works with
# no ECR access; a deploy pipeline can override it with a mirrored tag to stay
# clear of Docker Hub's anonymous rate limit.
ARG BUN_IMAGE=oven/bun:1.4.0-slim
# GH_IMAGE: where the gh BINARY comes from. The default names the `gh-source`
# stage below, so a local build compiles it; an override points at a prebuilt
# image, and under BuildKit an unreferenced stage never runs — which takes the
# clone and the Go module download out of the build entirely.
ARG GH_IMAGE=gh-source

# --- gh builder -------------------------------------------------------------
# gh is BUILT, not downloaded: image scanners read the Go toolchain and module
# versions embedded in the binary, and upstream's release artifact carries
# whatever versions it was built with — only a rebuild can move those.
# CGO is off and GOARCH comes from TARGETARCH, so the output is a static
# binary for the target arch. Pulled from AWS's mirror of the Docker official
# images rather than Docker Hub (no anonymous rate limit, no credentials).
#
# `--platform=$BUILDPLATFORM` pins the toolchain to the *builder's* own
# architecture so the Go compile runs natively and cross-compiles via GOARCH,
# instead of running under emulation on an x86 host. BUILDPLATFORM (like
# TARGETARCH below) requires BuildKit, which Docker Desktop and the GitHub
# runners use by default.
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:1.26.6-trixie AS gh-build
ARG GH_VERSION
# Auto-populated by buildx; the default keeps a plain `docker build` working.
ARG TARGETARCH=arm64
RUN git clone --depth=1 --branch "v${GH_VERSION}" https://github.com/cli/cli.git /src
WORKDIR /src
# x/mod v0.40.0 is the fix line; it also carries x/crypto, x/net, x/text and
# x/tools forward. grpc v1.83.2 clears CVE-2026-84304 (unauthenticated remote
# OOM via fragmented HTTP/2 DATA frames). `tidy` reconciles go.sum.
RUN go get golang.org/x/mod@v0.40.0 google.golang.org/grpc@v1.83.2 \
    && go mod tidy
# VCS stamping stays ON: editing go.mod makes the tree dirty, so the module
# records as `v2.97.0+dirty` — mildly ugly, but it keeps the version in the
# binary's build info. `-buildvcs=false` would record `(devel)` and hide any
# future cli/cli module CVE from scanners entirely.
RUN CGO_ENABLED=0 GOOS=linux GOARCH="${TARGETARCH}" go build -trimpath \
        -ldflags "-s -w -X github.com/cli/cli/v2/internal/build.Version=v${GH_VERSION}" \
        -o /out/gh ./cmd/gh

# The publishable shape of the above: JUST the static binary, on scratch, so a
# mirror image and the local fallback are the same artifact built by the same
# instructions.
FROM scratch AS gh-source
COPY --from=gh-build /out/gh /out/gh

# --- AWS CLI builder --------------------------------------------------------
# AWS CLI v2 from Amazon's official installer, in its own stage so the zip,
# unzip and the installer's scratch files never reach the runtime layer.
# v2 (not Debian's `awscli`, which is the Python v1 line) because the profile
# below uses `credential_process`, and because v2 is the CLI every AWS doc and
# every skill will assume. Installed size is ~230 MB; see
# docs/capabilities/aws-readonly.md for the trade-off.
FROM ${BUN_IMAGE} AS awscli-build
ARG TARGETARCH=arm64
RUN export DEBIAN_FRONTEND=noninteractive \
    && apt-get update \
    && apt-get install -y --no-install-recommends curl unzip ca-certificates \
    && case "${TARGETARCH}" in \
         arm64) AWS_ARCH=aarch64 ;; \
         amd64) AWS_ARCH=x86_64 ;; \
         *) echo "unsupported TARGETARCH ${TARGETARCH}" && exit 1 ;; \
       esac \
    && curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_ARCH}.zip" -o /tmp/awscliv2.zip \
    && unzip -q /tmp/awscliv2.zip -d /tmp \
    && /tmp/aws/install --install-dir /opt/aws-cli --bin-dir /opt/aws-cli/bin \
    && rm -rf /tmp/aws /tmp/awscliv2.zip /var/lib/apt/lists/*

# Resolve whichever of the two GH_IMAGE names. `COPY --from=gh-provider`
# below is what makes the choice load-bearing.
FROM ${GH_IMAGE} AS gh-provider

# --- runtime ----------------------------------------------------------------
FROM ${BUN_IMAGE}

ARG FOUNDATION_GITHUB_APP_ID=0

# NOTE: AWS_EC2_METADATA_DISABLED must NOT be set container-wide: AgentCore
# vends the execution role's credentials via an IMDS-style metadata endpoint,
# and disabling it globally leaves the container with no credentials at all.
# AWS_EC2_METADATA_SERVICE_ENDPOINT: AgentCore serves those credentials on an
# IMDS-compatible endpoint but exposes none of the env signals omp's
# credential probe checks — without this pointer the omp Bedrock provider
# refuses with "No API key found". The real IMDS control is the tool policy
# denying 169.254.169.254, not an env var a raw curl could ignore.
ENV AWS_EC2_METADATA_SERVICE_ENDPOINT=http://169.254.169.254 \
    NODE_ENV=production \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    # omp env sheet: relocate the agent base to the baked container dir
    # (models.yml/materialized config land where omp's registry reads them;
    # ambient files like history.db stay off ~) and pin the fail-hard
    # security overlay.
    PI_CODING_AGENT_DIR=/opt/sky/omp-agent \
    PI_CONFIG_FILES=/opt/sky/omp-agent/security-overlay.yml \
    PI_NO_TITLE=1 \
    PI_NO_PTY=1

# Python for the omp eval tool (kernelMode "per-call"). The bun base is trixie
# (Debian 13), which ships 3.13 — over the 3.10+ floor. ca-certificates for
# TLS to Slack. curl: the git credential helper and the gh wrapper talk to the
# loopback broker with it. poppler-utils supplies `pdftotext`, the ONLY way a
# PDF dropped in a thread becomes readable text (files.ts shells out to it
# rather than adding a PDF parser dependency); missing here, every PDF
# silently degrades to a path the model cannot open.
#
# `upgrade` (not just `install`) is what picks up Debian security patches for
# packages the base image baked in — without it, glibc/systemd/libcap2/liblzma5
# stay at whatever oven/bun published. Naming the --only-upgrade set also
# changes this layer's hash, so a rebuild re-fetches patches rather than
# reusing a stale cached layer.
RUN export DEBIAN_FRONTEND=noninteractive \
    && apt-get update \
    && apt-get upgrade -y \
    && apt-get install -y --only-upgrade \
        libc6 libc-bin libsystemd0 libudev1 libcap2 liblzma5 sed \
    && apt-get install -y --no-install-recommends \
        python3 python3-venv python3-pip ca-certificates git curl \
        poppler-utils \
    && ln -sf /usr/bin/python3 /usr/local/bin/python \
    && rm -rf /var/lib/apt/lists/*

# --- Ambient GitHub (github-broker.ts) --------------------------------------
# gh CLI as /opt/sky/bin/gh-real (from the gh-provider stage above), fronted
# by a wrapper that injects the broker's short-lived token as GH_TOKEN. git
# authenticates through the `sky` credential helper wired in /etc/gitconfig,
# so plain `git clone` and `gh pr create` work in the sandbox with no
# model-facing credential tools.
RUN mkdir -p /opt/sky/bin
COPY --from=gh-provider /out/gh /opt/sky/bin/gh-real
RUN /opt/sky/bin/gh-real --version
COPY packages/infra/agent-image/bin/git-credential-sky \
     packages/infra/agent-image/bin/gh \
     packages/infra/agent-image/bin/aws \
     /opt/sky/bin/
# System-level gitconfig: github.com credentials come from the helper (git
# resolves `helper = sky` to git-credential-sky on PATH). useHttpPath makes
# git send the repo path in the credential request, so the helper sees which
# repo is being authenticated. Commits attribute to the App bot identity:
# <app-id>+<app-slug>[bot] is what links a commit to the GitHub App.
RUN chmod 0755 /opt/sky/bin/git-credential-sky /opt/sky/bin/gh /opt/sky/bin/aws /opt/sky/bin/gh-real \
    && printf '[credential "https://github.com"]\n\thelper = sky\n\tuseHttpPath = true\n[user]\n\tname = sky[bot]\n\temail = %s+sky[bot]@users.noreply.github.com\n' "${FOUNDATION_GITHUB_APP_ID}" > /etc/gitconfig
# --- Ambient AWS (aws-broker.ts) --------------------------------------------
# AWS CLI v2 as /opt/sky/bin/aws-real, fronted by the wrapper copied above.
# /opt/sky/aws is where server.ts WRITES the generated profile file at boot,
# one profile per configured read-only role; it is created here (owned by the
# runtime user) because the container cannot mkdir under a root-owned /opt.
COPY --from=awscli-build /opt/aws-cli /opt/aws-cli
RUN ln -s /opt/aws-cli/v2/current/bin/aws /opt/sky/bin/aws-real \
    && /opt/sky/bin/aws-real --version \
    && mkdir -p /opt/sky/aws

# /opt/sky/bin FIRST so `gh` and `aws` resolve to the wrappers, never the real
# binaries.
ENV PATH="/opt/sky/bin:${PATH}"

# Non-root runtime user.
RUN groupadd --gid 10001 sky \
    && useradd --uid 10001 --gid 10001 --create-home --shell /usr/sbin/nologin sky

WORKDIR /app

# Workspace install with the frozen lockfile (the build context is the repo
# root). Workspaces the agent does not run are copied as their MANIFEST ONLY:
# `bun install --frozen-lockfile` fails with `Workspace not found` if the
# directory is absent, and this satisfies resolution without shipping source.
COPY package.json bun.lock bunfig.toml tsconfig.json ./
# Every workspace the agent actually runs, in full. `bun install
# --frozen-lockfile` fails with `Workspace not found` if a workspace the
# lockfile names is absent, so the ones this image does NOT run are copied as
# their MANIFEST ONLY: enough to satisfy resolution, without shipping source.
COPY packages/core/ ./packages/core/
COPY packages/connectors/ ./packages/connectors/
COPY packages/runtime/ ./packages/runtime/
COPY packages/capability-otter/ ./packages/capability-otter/
COPY packages/capability-knock/ ./packages/capability-knock/
COPY packages/capability-browser/ ./packages/capability-browser/
COPY packages/capability-email/ ./packages/capability-email/
COPY packages/capability-crm/ ./packages/capability-crm/
COPY packages/capability-upwork/ ./packages/capability-upwork/
COPY packages/agent/ ./packages/agent/
# Manifest only: the gateway and every proxy are Lambdas, the newsletter is a
# Lambda service, and the infra package is the CDK app a pipeline runs. None of
# them executes in this container.
COPY packages/testing/package.json ./packages/testing/package.json
COPY packages/gateway/package.json ./packages/gateway/package.json
COPY packages/newsletter/package.json ./packages/newsletter/package.json
COPY packages/infra/package.json ./packages/infra/package.json

# Enforce the pin's SHAPE before installing — an EXACT version, no ^/~ range.
# Deliberately not a literal version: asserting one breaks the image build the
# first time the pin is legitimately bumped, and package.json plus
# --frozen-lockfile already pin the version itself.
RUN grep -qE '"@oh-my-pi/pi-coding-agent": "[0-9]+\.[0-9]+\.[0-9]+"' packages/agent/package.json \
    || (echo "FATAL: packages/agent must pin @oh-my-pi/pi-coding-agent to an EXACT version (no ^/~)" && exit 1)
RUN bun install --frozen-lockfile --production

# The microVM's writable workspace (repo clones, and the memory/skills
# materialization target /workspace/.omp/).
RUN mkdir -p /workspace && chown sky:sky /workspace

# Baked omp agent base: the fail-hard security overlay, an INTENTIONAL empty
# AGENTS.md (shadows any ambient user-level context file), and ownership for
# the runtime user — the materializer writes models.yml here at session build
# and would EACCES on an un-created, root-owned /opt path.
COPY packages/infra/agent-image/security-overlay.yml /opt/sky/omp-agent/security-overlay.yml
RUN touch /opt/sky/omp-agent/AGENTS.md \
    && chown -R sky:sky /opt/sky/omp-agent /opt/sky/aws

USER sky:sky

EXPOSE 8080

# Entrypoint contract (packages/agent): server.ts starts the :8080
# /invocations + /ping server, runs the RUNTIME_SECRET_ID bootstrap (key COUNT
# logged only), and dispatches handleAgentInvocation — including
# {"_smoke_test": true}.
CMD ["bun", "run", "packages/agent/src/server.ts"]
