Files
roboco/.github/workflows/release.yml
T
c70ff3cf9a feat(providers): Codex CLI provider — OpenAI via ModelProvider.OPENAI (#659)
* feat(providers): Codex CLI provider — OpenAI via ModelProvider.OPENAI

Mirrors the grok blueprint end to end: CodexCliProvider (RO ~/.codex
mount, ANTHROPIC_* blanked), an orchestrator-side codex_auth.py
refresher (JWT-exp staleness, atomic rewrite, lock-serialized single-use
rotation, --check backstop; the CLI's own in-process refresh write
no-ops on the RO mount by design — margins keep the orchestrator ahead
of the CLI's 5-minute window), config.toml rendering with required=true
gateway MCP servers, execpolicy deny rules (forbidden-only), per-role
--sandbox (developer=workspace-write, review/doc roles read-only),
codex exec --json with pinned ROBOCO_CODEX_CLI_MODEL (gpt-5.3-codex),
usage summed from typed turn.completed events priced via the real
4-bucket split, dedicated image + entrypoint, registry/park/finalize/
compose/release wiring. V1 excludes interactive intake/secretary.

Per adversarial review: migration 083 seeds the openai provider row
enabled=True (without it every routing path 404'd — the whole feature
was operationally dead code; grok needed the same seed in 039), the
panel picker gained the OpenAI catalog group it silently lacked, and
exit classification is structural — only stderr and error.message
fields from error events are sniffed (word-boundaried patterns, exact
auth phrases, bare 'login' dropped), so the model echoing on-topic
words can never false-park the provider fleet-wide, proven by a
benign-transcript test. Known open risk flagged, not claimed: whether
codex's workspace-write OS sandbox excludes /app is unverified, and no
hook mechanism exists to port the bash-guard defense-in-depth.

* fix(providers): containment barrier on usage.json reads (code scanning)

CodeQL flagged the codex usage read as path injection — correctly:
os.path.basename does not neutralize '..', and the upstream segment
validator isn't in CodeQL's taint model. The grok/codex reads collapse
into one _read_usage_json_contained helper that resolves the built path
and refuses anything outside the resolved usage root — a hostile id can
never escape regardless of upstream drift. Traversal + containment
regression tests added; a stray noqa in the test file replaced with a
named constant per repo rule.

* fix(providers): use realpath+startswith containment CodeQL recognizes

The is_relative_to() guard was a real barrier but not in CodeQL's
py/path-injection sanitizer model, so the alert persisted. Switch to
the canonical os.path.realpath + startswith(root + os.sep) form, which
CodeQL recognizes as a path-traversal barrier; behavior is identical
(refuse any candidate resolving outside the usage root).

* fix(providers): regexp-allowlist the usage-id segment (CodeQL barrier)

Neither is_relative_to nor realpath+startswith was recognized by
CodeQL's py/path-injection sanitizer model across the str->Path->open
flow. Sanitize the tainted component at the source instead: the id must
fullmatch a strict slug token ([A-Za-z0-9][A-Za-z0-9._-]*, no
separators, no '..'), which CodeQL recognizes as a path-injection
barrier; the realpath+startswith containment stays as defense-in-depth.

* fix(providers): standalone regexp guard so CodeQL recognizes the barrier

The sanitizer was one disjunct of a compound 'or' condition, which
CodeQL's guard analysis does not trace as a barrier. Split the regexp
fullmatch into its own single-condition guard (the redundant '..' check
is dropped — the required alphanumeric first char already excludes it).

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 03:20:29 +02:00

191 lines
7.9 KiB
YAML

name: Release
on:
release:
types: [published]
workflow_dispatch:
jobs:
publish-images:
name: Build & push all RoboCo images to GHCR + Docker Hub
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
version: ${{ steps.ver.outputs.version }}
env:
GHCR: ghcr.io/rennf93 # GitHub Container Registry namespace
DOCKERHUB: docker.io/renzof93 # Docker Hub namespace (different username)
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Free up runner disk space
run: |
# Eleven images on one runner is disk-heavy; drop preinstalled tooling
# we don't use so the builds don't run out of space.
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL || true
df -h /
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: renzof93
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Derive version tag
id: ver
run: |
# release → the tag (v0.1.0 → 0.1.0). Any manual dispatch runs against a
# branch whose name can contain "/" (e.g. feature/x) — not a valid image
# tag — so use the short SHA, which always is one.
if [ "${{ github.event_name }}" = "release" ]; then
RAW="${{ github.event.release.tag_name }}"
VERSION="${RAW#v}"
else
VERSION="$(git rev-parse --short HEAD)"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Release version: $VERSION"
- name: Build & push every RoboCo image
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
# Tag one built image for both registries at :VERSION and :latest.
regtags() {
local name="$1"
echo "-t ${GHCR}/${name}:${VERSION} -t ${GHCR}/${name}:latest" \
"-t ${DOCKERHUB}/${name}:${VERSION} -t ${DOCKERHUB}/${name}:latest"
}
# Push one image's four tags (both registries, both labels).
pushall() {
local name="$1"
for ref in "${GHCR}/${name}" "${DOCKERHUB}/${name}"; do
docker push "${ref}:${VERSION}"
docker push "${ref}:latest"
done
}
# agent-base MUST build first: the agent images build
# `FROM roboco-agent-base` — a local tag that has to exist in the daemon
# before they build. Tag it locally (for the FROM) and for both registries.
echo "::group::build roboco-agent-base"
docker build -f docker/agent-base.Dockerfile \
-t roboco-agent-base $(regtags roboco-agent-base) .
echo "::endgroup::"
# roboco-agent-grok MUST build next, ahead of the loop: the two
# interactive Grok roles (prompter, secretary) build `FROM
# roboco-agent-grok` — a local tag that has to exist in the daemon
# before they build, and bash associative-array iteration order is
# unspecified, so it can't just be another entry in IMAGES below.
echo "::group::build roboco-agent-grok"
docker build -f docker/agent-grok.Dockerfile \
-t roboco-agent-grok $(regtags roboco-agent-grok) .
echo "::endgroup::"
# Every other image → its Dockerfile. Names mirror docker-compose's
# `image:` values exactly, so compose can later pull instead of build.
declare -A IMAGES=(
[roboco-orchestrator]=docker/orchestrator.Dockerfile
[roboco-panel]=docker/panel.Dockerfile
[roboco-video-renderer]=docker/video-renderer.Dockerfile
[roboco-sandbox-pg]=docker/sandbox-pg.Dockerfile
[roboco-agent-pm]=docker/agent-pm.Dockerfile
[roboco-agent-dev-be]=docker/agent-dev-be.Dockerfile
[roboco-agent-dev-fe]=docker/agent-dev-fe.Dockerfile
[roboco-agent-qa-be]=docker/agent-qa-be.Dockerfile
[roboco-agent-qa-fe]=docker/agent-qa-fe.Dockerfile
[roboco-agent-ux]=docker/agent-ux.Dockerfile
[roboco-agent-doc]=docker/agent-doc.Dockerfile
[roboco-agent-prompter]=docker/agent-prompter.Dockerfile
[roboco-agent-secretary]=docker/agent-secretary.Dockerfile
[roboco-agent-pr-reviewer]=docker/agent-pr-reviewer.Dockerfile
[roboco-agent-grok-prompter]=docker/agent-grok-prompter.Dockerfile
[roboco-agent-grok-secretary]=docker/agent-grok-secretary.Dockerfile
[roboco-agent-codex]=docker/agent-codex.Dockerfile
)
for name in "${!IMAGES[@]}"; do
echo "::group::build ${name}"
docker build -f "${IMAGES[$name]}" $(regtags "${name}") .
echo "::endgroup::"
done
# Push only after every build succeeds, so a failure never leaves a
# half-published release. Base first, then the rest.
echo "::group::push roboco-agent-base"
pushall roboco-agent-base
echo "::endgroup::"
echo "::group::push roboco-agent-grok"
pushall roboco-agent-grok
echo "::endgroup::"
for name in "${!IMAGES[@]}"; do
echo "::group::push ${name}"
pushall "${name}"
echo "::endgroup::"
done
echo "Published roboco-agent-base + roboco-agent-grok + ${#IMAGES[@]} more images to GHCR + Docker Hub at :${VERSION} and :latest"
pull-smoke:
name: Registry pull smoke — verify every published image is pullable
needs: publish-images
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
# This is the exact regression class the docs/CHANGELOG already record
# once ("Release images complete" — the release workflow's build matrix
# was missing the two Grok sub-images, so docker-compose.registry.yml's
# pull failed for every fresh user until the next release). Running the
# literal command a user runs, against the tag that was just published,
# catches a missing/mistagged image before anyone else does.
env:
ROBOCO_REGISTRY: ghcr.io/rennf93
ROBOCO_VERSION: ${{ needs.publish-images.outputs.version }}
# docker-compose.registry.yml hard-requires these three non-empty
# (`${VAR:?...}` on the orchestrator/nginx services) just to
# INTERPOLATE the file — `pull` never touches their runtime values, so
# placeholders are enough to get past that gate (see scripts/bootstrap.sh
# for what a real deploy generates here).
ROBOCO_ENCRYPTION_KEY: pull-smoke-placeholder-not-a-real-key
ROBOCO_AGENT_AUTH_SECRET: pull-smoke-placeholder-not-a-real-secret
ROBOCO_PANEL_AGENT_TOKEN: pull-smoke-placeholder-not-a-real-token
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Free up runner disk space
run: |
# Same trick as publish-images: pulling ~20 images (roboco's own
# plus postgres/redis/ollama/nginx) on one runner is disk-heavy.
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL || true
df -h /
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull every image docker-compose.registry.yml references
run: |
echo "Pulling ${ROBOCO_REGISTRY}/roboco-*:${ROBOCO_VERSION} (+ upstream deps) — failing on any missing image..."
docker compose -f docker-compose.registry.yml pull