4f4604eabe
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 2s
CI / Template validation (static) (pull_request) Successful in 28s
CI / Template validation (static) (push) Successful in 1m10s
CI / Adapter unit tests (push) Successful in 1m17s
CI / T4 tier-4 conformance (live) (push) Failing after 4s
CI / Adapter unit tests (pull_request) Successful in 1m11s
CI / Template validation (runtime) (pull_request) Successful in 3m41s
CI / Template validation (runtime) (push) Successful in 3m33s
CI / T4 tier-4 conformance (live) (pull_request) Successful in 3m44s
CI / validate (push) Failing after 1s
CI / validate (pull_request) Successful in 1s
Image-side companion to molecule-core PR #1525 (merge_sha 73a09443a086, workspace-server applyAgentGitIdentity). PR #1525 sets GIT_ASKPASS= /usr/local/bin/molecule-askpass on every workspace container so git can authenticate to private HTTPS remotes from the persona env vars already arriving via workspace_secrets — but until this binary ships in the runtime image, git invocations error with 'exec: /usr/local/bin/ molecule-askpass: not found' (forward-only pin gap). This is the same class as Hermes list_peers / codex #219: ws-server changed contract, runtime image hadn't yet caught up. Closing the image-side gap unblocks Dev-A/Dev-B (claude-code runtime) durable HTTPS git auth on any private host. Generic by design — no hardcoded hostnames, no vendor literals. Script body is identical to workspace/scripts/molecule-askpass in molecule-core and the parallel external workspace template repos, so any deployer can fork this template and use it against their own git host without editing.
151 lines
8.1 KiB
Docker
151 lines
8.1 KiB
Docker
FROM python:3.11-slim
|
|
|
|
# System deps — curl/gosu/node/npm for the runtime; git + gh for agent
|
|
# autonomy (agents run `gh issue list`, `gh issue create`, `gh issue edit
|
|
# --add-assignee`, `git clone`, etc. per their idle/cron prompts).
|
|
# Without these the team's claim-and-ship loop silently returns
|
|
# "(no response generated)" because tools error out.
|
|
#
|
|
# T4 escalation leg (RFC internal#456 §9 / PR#474):
|
|
# sudo + util-linux(nsenter) + docker.io(CLI) are baked here so the
|
|
# uid-1000 `agent` (see useradd below — UNCHANGED, agent stays
|
|
# uid-1000) has a wired, audited path to host root inside the
|
|
# provisioner's `--privileged --pid=host -v /:/host
|
|
# -v /var/run/docker.sock:/var/run/docker.sock` container. Without
|
|
# sudo, a uid-1000 process in --privileged CANNOT nsenter/chroot
|
|
# /host (--privileged grants caps to root, not uid-1000) and cannot
|
|
# use the root:docker 0660 docker.sock — T4 would be
|
|
# provisioner-shape-only (the documented ABSENT-escalation-leg gap).
|
|
# The sudoers drop-in + docker-group add are below, after useradd,
|
|
# so `agent` exists. This is ADDITIVE: it does NOT change the agent
|
|
# uid and does NOT change /configs token ownership (still uid-1000,
|
|
# enforced by entrypoint.sh + the Layer-3 conformance gate).
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
curl gosu nodejs npm ca-certificates git sudo util-linux docker.io \
|
|
&& install -m 0755 -d /etc/apt/keyrings \
|
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
|
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \
|
|
&& apt-get update && apt-get install -y --no-install-recommends gh \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Install claude-code CLI via npm
|
|
RUN npm install -g @anthropic-ai/claude-code 2>/dev/null || true
|
|
|
|
# Create agent user — UNCHANGED. The agent runs as uid-1000; the T4
|
|
# escalation leg below is additive and does NOT promote the agent to
|
|
# root. claude-code still refuses --dangerously-skip-permissions as
|
|
# root, and /configs/.auth_token must stay agent-owned (Hermes
|
|
# list_peers 401 class — RFC internal#456 §10).
|
|
RUN useradd -u 1000 -m -s /bin/bash agent
|
|
|
|
# --- T4 escalation leg (RFC internal#456 §9.3 / PR#474) ---
|
|
# Wired path: uid-1000 agent -> host root inside the provisioner's
|
|
# --privileged --pid=host -v /:/host -v docker.sock container.
|
|
# 1. NOPASSWD sudoers drop-in (mode 0440, visudo-validated at build
|
|
# so a malformed sudoers can never ship a broken-sudo image).
|
|
# 2. agent in the `docker` group so the bind-mounted root:docker
|
|
# 0660 /var/run/docker.sock is usable without sudo.
|
|
# Atomic co-sequencing (RFC §10): this ships in the SAME image
|
|
# revision as the uid-1000 + agent-owned-token entrypoint contract;
|
|
# the Layer-3 conformance gate asserts BOTH on the running container.
|
|
RUN set -eux; \
|
|
printf 'agent ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/agent-t4; \
|
|
chmod 0440 /etc/sudoers.d/agent-t4; \
|
|
visudo -cf /etc/sudoers.d/agent-t4; \
|
|
groupadd -f docker; \
|
|
usermod -aG docker agent; \
|
|
id agent
|
|
|
|
WORKDIR /app
|
|
|
|
# RUNTIME_VERSION is forwarded from the reusable publish workflow as
|
|
# a docker build-arg. When set (cascade-triggered builds), it's the
|
|
# exact runtime version PyPI just published. Including it as an ARG
|
|
# changes the cache key for the pip install layer below — without
|
|
# this, identical Dockerfile + identical requirements.txt content
|
|
# would let docker reuse the cached layer with the previous version
|
|
# baked in (the cache trap that bit us 5x on 2026-04-27).
|
|
# Empty default = falls back to whatever requirements.txt resolves to.
|
|
ARG RUNTIME_VERSION=
|
|
|
|
# Install Python deps. The RUNTIME_VERSION ARG is a no-op argument to
|
|
# the RUN command itself but its presence as a declared ARG above
|
|
# means buildx hashes it into the cache key.
|
|
COPY requirements.txt .
|
|
RUN pip install --no-cache-dir -r requirements.txt && \
|
|
if [ -n "${RUNTIME_VERSION}" ]; then \
|
|
pip install --no-cache-dir --upgrade "molecule-ai-workspace-runtime==${RUNTIME_VERSION}"; \
|
|
fi
|
|
|
|
# Copy adapter code
|
|
COPY adapter.py .
|
|
COPY __init__.py .
|
|
# Provider registry. The adapter's _load_providers walks 4 paths:
|
|
# 1. /opt/adapter/config.yaml — provisioner-managed canonical
|
|
# 2. os.path.dirname(__file__)/config.yaml — alongside adapter.py (this image)
|
|
# 3. ${WORKSPACE_CONFIG_PATH}/config.yaml — workspace per-instance overrides
|
|
# 4. _BUILTIN_PROVIDERS — oauth + anthropic-api only
|
|
# On this image /opt/adapter/ is never populated by the platform
|
|
# provisioner, so path 2 (/app/config.yaml) is the load-bearing one.
|
|
# Without this COPY the file isn't in the image, all 3 file paths fail,
|
|
# and _load_providers falls through to _BUILTIN_PROVIDERS — every
|
|
# MiniMax/GLM/Kimi/DeepSeek model silently routes to anthropic-oauth →
|
|
# "Not logged in. Please run /login" at first LLM call. Caused the
|
|
# canary's 38h chronic red on 2026-05-07/08 (molecule-core#129).
|
|
COPY config.yaml .
|
|
# Adapter-specific executor — owned by THIS template (universal-runtime
|
|
# refactor, molecule-core task #87). Lives alongside adapter.py so
|
|
# Python's import system picks the local /app/claude_sdk_executor.py
|
|
# before the same-named module that older molecule-runtime versions
|
|
# also shipped under site-packages. Once molecule-core drops the file
|
|
# from its workspace/ package and bumps the runtime PyPI version, the
|
|
# template will be the sole source of truth.
|
|
COPY claude_sdk_executor.py .
|
|
|
|
# Set the adapter module for runtime discovery
|
|
ENV ADAPTER_MODULE=adapter
|
|
|
|
# Git credential helper + background refresh daemon — fix for #1933 / #1866 / #547.
|
|
# Without these, GH_TOKEN injected at provision time expires after ~60 min
|
|
# and every subsequent git push/clone returns 401, causing agents to
|
|
# infinite-loop status reports back to PMs and overflow A2A queues.
|
|
#
|
|
# The helper hits the platform's /admin/github-installation-token endpoint
|
|
# (and falls back to env-var GH_TOKEN when platform is unreachable). The
|
|
# refresh daemon calls _refresh_gh every ~45 min so `gh` CLI auth and the
|
|
# helper cache stay warm even when no git operation triggers a refresh.
|
|
COPY scripts/molecule-git-token-helper.sh /app/scripts/molecule-git-token-helper.sh
|
|
COPY scripts/molecule-gh-token-refresh.sh /app/scripts/molecule-gh-token-refresh.sh
|
|
RUN chmod +x /app/scripts/molecule-git-token-helper.sh /app/scripts/molecule-gh-token-refresh.sh
|
|
|
|
# Generic GIT_ASKPASS helper — image-side companion to molecule-core PR
|
|
# #1525 (workspace-server applyAgentGitIdentity, merge_sha 73a09443a086).
|
|
# Reads HTTPS Basic-Auth credentials from env vars (GIT_HTTP_USERNAME /
|
|
# GIT_HTTP_PASSWORD, with GITEA_USER / GITEA_TOKEN as fallback) and emits
|
|
# them on the git credential-prompt protocol, so container-side `git` can
|
|
# authenticate to any private HTTPS remote without on-disk ~/.gitconfig
|
|
# or ~/.git-credentials mutation. The platform provisioner sets
|
|
# GIT_ASKPASS=/usr/local/bin/molecule-askpass via applyAgentGitIdentity;
|
|
# until this binary ships in the runtime image, git invocations error
|
|
# with "exec: /usr/local/bin/molecule-askpass: not found" (forward-only
|
|
# pin gap — same class as Hermes list_peers and codex template breakage,
|
|
# fixed image-side here).
|
|
#
|
|
# No hardcoded hostnames or vendor names — the script body is identical
|
|
# to the one shipped in molecule-core workspace/scripts/molecule-askpass
|
|
# and the parallel external workspace template repos, so any deployer
|
|
# can fork this template and use it against their own git host without
|
|
# editing.
|
|
COPY scripts/molecule-askpass /usr/local/bin/molecule-askpass
|
|
RUN chmod +x /usr/local/bin/molecule-askpass
|
|
|
|
# Drop-priv entrypoint — claude-code refuses --dangerously-skip-permissions
|
|
# as root, so we run molecule-runtime as the agent user (uid 1000).
|
|
# The script handles volume-ownership fix + session-dir symlink before
|
|
# exec'ing via gosu.
|
|
COPY entrypoint.sh /entrypoint.sh
|
|
RUN chmod +x /entrypoint.sh
|
|
|
|
ENTRYPOINT ["/entrypoint.sh"]
|