Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions .github/workflows/build-bobshell.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: Build Bobshell Container

on:
push:
branches:
- main
paths:
- 'infra/bobshell/**'
- '.github/workflows/build-bobshell.yml'
pull_request:
branches:
- main
paths:
- 'infra/bobshell/**'
- '.github/workflows/build-bobshell.yml'
release:
types:
- published
workflow_dispatch:

env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/ibm/cfex-bobshell

jobs:
build:
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write

strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
include:
- platform: linux/amd64
runner: ubuntu-24.04
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest

- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=raw,value=${{ matrix.suffix }}-${{ github.sha }}

- name: Build and push container image
id: build
uses: docker/build-push-action@v5
with:
context: ./infra/bobshell
file: ./infra/bobshell/Containerfile
platforms: ${{ matrix.platform }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=bobshell-${{ matrix.suffix }}
cache-to: type=gha,mode=max,scope=bobshell-${{ matrix.suffix }}
provenance: false

- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
echo "Digest for bobshell-${{ matrix.suffix }}: $digest"

- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digest-bobshell-${{ matrix.suffix }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

manifest:
name: Create Manifest
needs: build
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write

steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest

- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create and push manifest
run: |
SHA=${{ github.sha }}
IMAGE=${{ env.IMAGE }}

echo "Creating multiplatform manifest for bobshell..."

IMAGES=(
"${IMAGE}:amd64-${SHA}"
"${IMAGE}:arm64-${SHA}"
)

if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
# Release publication: tag with the release name and latest.
docker buildx imagetools create \
--tag "${IMAGE}:${SHA}" \
--tag "${IMAGE}:${GITHUB_REF_NAME}" \
--tag "${IMAGE}:latest" \
"${IMAGES[@]}"
elif [[ "${{ github.event_name }}" == "push" ]] || [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
docker buildx imagetools create \
--tag "${IMAGE}:${SHA}" \
--tag "${IMAGE}:latest" \
"${IMAGES[@]}"
else
docker buildx imagetools create \
--tag "${IMAGE}:${SHA}" \
"${IMAGES[@]}"
fi

echo "Manifest created successfully"

- name: Inspect manifest
run: |
echo "Inspecting multiplatform manifest..."
docker buildx imagetools inspect "${{ env.IMAGE }}:${{ github.sha }}"
7 changes: 7 additions & 0 deletions infra/bobshell/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git
.gitignore
.env
secrets/
workspace/
docker-compose.yml
README.md
4 changes: 4 additions & 0 deletions infra/bobshell/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Never commit secrets or local state
secrets/
workspace/
.env
63 changes: 63 additions & 0 deletions infra/bobshell/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# syntax=docker/dockerfile:1
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# bobshell - IBM Bob Shell (agentic CLI) container image
#
# Based on ubi10/nodejs-24, which provides Node.js 24 (Bob Shell requires
# Node.js >= 22.15). Bob Shell is installed with the official installer, the
# IBM license is pre-accepted, and the /workspace directory is pre-trusted so
# the CLI runs non-interactively.
#
# Build (from this directory):
# docker build -f Containerfile -t bobshell .
#
# Pin a Bob Shell version:
# docker build --build-arg BOB_VERSION=2.0.5 -t bobshell .
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

FROM registry.access.redhat.com/ubi10/nodejs-24-minimal:latest

# Workspace Bob Shell operates in. Owned by the default user (1001), group
# root with g=u so arbitrary UIDs in gid 0 (OpenShift-style) can also use it.
USER root
RUN mkdir -p /workspace \
&& chown 1001:0 /workspace \
&& chmod g=u /workspace
COPY --chmod=0755 entrypoint.sh /usr/local/bin/bob-entrypoint
USER 1001

# Install Bob Shell via the official installer. The installer verifies the
# package SHA256 and runs `npm install -g`; the image's npm global prefix
# (/opt/app-root/src/.npm-global) is writable by user 1001 and on PATH.
ARG BOB_VERSION=""
RUN set -eu; \
curl -fsSL https://bob.ibm.com/download/bobshell.sh -o /tmp/bobshell.sh; \
if [ -n "${BOB_VERSION}" ]; then \
bash /tmp/bobshell.sh --pm npm --version "${BOB_VERSION}"; \
else \
bash /tmp/bobshell.sh --pm npm; \
fi; \
rm -f /tmp/bobshell.sh /tmp/bobshell-*.tgz /tmp/bobshell-install.log; \
command -v bob; \
bob --version

# Pre-accept the IBM license and pre-trust /workspace from the command line
# so Bob Shell never blocks on first-run prompts. State file formats match
# what Bob Shell 2.0.x itself writes:
# ~/.bob/settings/settings.json <- "licenseConsent": true (`bob --accept-license`)
# ~/.bob/trustedFolders.json <- path -> "TRUST_PARENT" (`bob chat --trust`)
# The default CMD also passes --accept-license/--trust, so a fresh volume
# mounted over $HOME (shadowing these files) still starts without prompts.
RUN mkdir -p "${HOME}/.bob/settings" \
&& printf '{\n "licenseConsent": true\n}\n' > "${HOME}/.bob/settings/settings.json" \
&& printf '{\n "/workspace": "TRUST_PARENT"\n}\n' > "${HOME}/.bob/trustedFolders.json"

WORKDIR /workspace

LABEL org.opencontainers.image.title="bobshell"
LABEL org.opencontainers.image.description="IBM Bob Shell agentic CLI on UBI 10 / Node.js 24"
LABEL org.opencontainers.image.source="https://github.com/IBM/contextforge-examples"
LABEL org.opencontainers.image.vendor="IBM"
LABEL org.opencontainers.image.licenses="Apache-2.0"

ENTRYPOINT ["bob-entrypoint"]
CMD ["bob", "chat", "--accept-license", "--trust"]
105 changes: 105 additions & 0 deletions infra/bobshell/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Bobshell Container

[IBM Bob Shell](https://bob.ibm.com/docs/shell) (the `bob` agentic CLI) packaged
as a container image on `registry.access.redhat.com/ubi10/nodejs-24`.

- Bob Shell installed via the [official installer](https://bob.ibm.com/docs/shell/getting-started/install-and-setup)
(SHA256-verified, `npm install -g`).
- IBM license pre-accepted and `/workspace` pre-trusted at image build time, so
`bob` never blocks on first-run prompts (see
[trusted folders](https://bob.ibm.com/docs/shell/security/trusted-folders)).
- Runs as the non-root default user (UID 1001).
- Defaults to an interactive `bob chat` session when the container starts.

## Configuration

The container accepts two configurations:

| Configuration | Mechanism | Container location | Effect |
| ------------- | --------- | ------------------ | ------ |
| Bobshell API secret | Compose `secrets:` (or `BOB_API_KEY` env var) | `/run/secrets/bob_api_key` | Exported as `BOB_API_KEY`; used for [API key authentication](https://bob.ibm.com/docs/shell/getting-started/install-and-setup#api-key-authentication) |
| MCP configuration | Compose `configs:` | `/etc/bob/mcp.json` | Installed as `~/.bob/settings/mcp.json` (user-global [`mcpServers`](https://bob.ibm.com/docs/shell/configuration/mcp/mcp-bobshell) config) unless one already exists |

An API key (scope **Inference**) is created in the
[Bob web portal](https://bob.ibm.com/login).

## Quick start (Docker Compose)

```bash
cd infra/bobshell

# Provide the API secret and a project directory
mkdir -p secrets workspace
printf '%s' 'your-bob-api-key' > secrets/bob_api_key.txt
# Change permission on the secrets directory:
chmod 700 secrets

Comment thread
jonpspri marked this conversation as resolved.
# Change permissions on the secrets file:
chmod 600 secrets/bob_api_key.txt
# Edit the sample MCP configuration to taste
$EDITOR mcp.json

# Interactive session (image is pulled or built locally)
docker compose run --rm bobshell

# Headless task
docker compose run --rm bobshell bob run --accept-license --trust "Explain this project"

# Build locally instead of pulling the published image
docker compose build
```

`docker-compose.yml` mounts `./workspace` at `/workspace` (the pre-trusted
working directory) and persists `~/.bob` (sessions, tasks, settings) in the
`bob-state` named volume.

## Plain Docker

```bash
docker build -f Containerfile -t bobshell .

# Interactive
docker run --rm -it \
-e BOB_API_KEY="your-bob-api-key" \
-v "$PWD/workspace:/workspace" \
bobshell

# Headless, with MCP config mounted
docker run --rm \
-e BOB_API_KEY="your-bob-api-key" \
-v "$PWD/mcp.json:/etc/bob/mcp.json:ro" \
-v "$PWD/workspace:/workspace" \
bobshell bob run --accept-license --trust "Summarize this repo"
```

Any command after the image name replaces the default `bob chat` invocation;
a leading flag (e.g. `--version`) is passed to `bob` itself.

## Version pinning

The image installs the latest Bob Shell release by default. Pin a version at
build time:

```bash
docker build --build-arg BOB_VERSION=2.0.5 -t bobshell .
```

## Publishing (CI)

`.github/workflows/build-bobshell.yml` builds `linux/amd64` and `linux/arm64`
images and publishes a multi-arch manifest to
`ghcr.io/ibm/cfex-bobshell` on:

- pushes to `main` touching `infra/bobshell/**` (tagged with the commit SHA and `latest`),
- **release publication** (additionally tagged with the release tag),
- manual `workflow_dispatch`.

## Security notes

- Never commit `secrets/` or set your API key in any tracked file; the local
`.gitignore` excludes them, and `.dockerignore` keeps them out of the build
context.
- The pre-trust entry covers `/workspace` only; Bob Shell's folder-trust
protections remain intact for everything else.
- Run the container with least privilege: it needs no host access beyond the
mounted workspace and outbound network to `bob.ibm.com`.
Loading
Loading