Skip to content

Commit c8edd2e

Browse files
ralyodioclaude
andcommitted
feat(pods): Claude Code + Codex in member pods (custom image)
Members can now code in their pod: build a custom pod image (FROM the base Ubuntu) that ships git, openssh-client, Node.js 22, and the Claude Code (`claude`) and Codex (`codex`) CLIs. BYO key — no credentials are baked in; a member exports their own ANTHROPIC_API_KEY / OPENAI_API_KEY (or uses the tools' login flow), stored in their persisted home. - pods/Containerfile: the image (also drops a BYO-key + git-push login hint). - setup.sh: build it on the host (rootless podman, layer-cached), switch AGENTBBS_POD_IMAGE to localhost/agentbbs-pod:latest (upserted for existing installs), keeping the base image if the build fails. - pods.go: image-aware self-heal — an idle pod on an out-of-date image is recreated (home volume kept) so the new tooling rolls out without a manual rebuild and without disturbing active sessions. Verified: image builds on the host; inside it node v22, git, ssh, `claude --version` (2.1.186) and `codex --version` (0.142.0) all run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c5489a4 commit c8edd2e

3 files changed

Lines changed: 94 additions & 2 deletions

File tree

internal/pods/pods.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,30 @@ func (m *Manager) hasMount(name, dest string) bool {
8787
return false
8888
}
8989

90+
// hasImage reports whether the named container is running the given image.
91+
// Used to roll out a new pod image: a mismatch triggers an idle recreate so
92+
// members pick up added tooling without losing their home volume. A blank or
93+
// unresolvable image name is treated as a match (never heal on uncertainty).
94+
func (m *Manager) hasImage(name, image string) bool {
95+
if image == "" {
96+
return true
97+
}
98+
out, err := exec.Command(m.engine, "container", "inspect", "-f", "{{.ImageName}}", name).Output()
99+
if err != nil {
100+
return true
101+
}
102+
got := strings.TrimSpace(string(out))
103+
// Normalize: inspect may report "localhost/agentbbs-pod:latest" while m.image
104+
// is the same; also tolerate the docker.io/library/ prefix podman adds.
105+
norm := func(s string) string {
106+
s = strings.TrimPrefix(s, "docker.io/library/")
107+
s = strings.TrimPrefix(s, "docker.io/")
108+
s = strings.TrimPrefix(s, "localhost/")
109+
return s
110+
}
111+
return norm(got) == norm(image)
112+
}
113+
90114
// Engine reports the active container engine.
91115
func (m *Manager) Engine() string { return m.engine }
92116

@@ -131,8 +155,13 @@ func (m *Manager) ensure(user string) (string, error) {
131155
m.mu.Lock()
132156
idle := m.attached[name] == 0
133157
m.mu.Unlock()
134-
if pubSpec != "" && idle && !m.hasMount(name, "/home/dev/public_html") {
135-
_ = exec.Command(m.engine, "rm", "-f", name).Run() // fall through to recreate with the bind
158+
// Recreate an idle pod when it's missing the public_html bind OR is
159+
// running an out-of-date image (e.g. a new pod image with added tooling).
160+
// The home volume persists across rm, so member data is kept; a busy pod
161+
// heals on its next idle attach instead.
162+
needsHeal := idle && ((pubSpec != "" && !m.hasMount(name, "/home/dev/public_html")) || !m.hasImage(name, m.image))
163+
if needsHeal {
164+
_ = exec.Command(m.engine, "rm", "-f", name).Run() // fall through to recreate
136165
} else {
137166
_ = exec.Command(m.engine, "start", name).Run() // no-op if running
138167
return name, nil

pods/Containerfile

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# AgentBBS member pod image. Built on the host by setup.sh (rootless podman),
2+
# tagged localhost/agentbbs-pod:latest, and used for every member pod via
3+
# AGENTBBS_POD_IMAGE. Members get a full shell here (HOME=/home/dev, persisted
4+
# in a named volume; ~/public_html is bind-mounted to their website).
5+
#
6+
# Beyond a base Ubuntu it ships:
7+
# - git + openssh-client → push to git.profullstack.com (SSH-key auth)
8+
# - Node.js (LTS) → runtime for the AI coding CLIs
9+
# - Claude Code + Codex CLIs → `claude` and `codex`, BYO API key per user
10+
#
11+
# BYO key: nothing here carries credentials. A member exports their own
12+
# ANTHROPIC_API_KEY / OPENAI_API_KEY (or runs the tools' login flow); the keys
13+
# live in their persisted home, never in the image.
14+
FROM docker.io/library/ubuntu:24.04
15+
16+
ENV DEBIAN_FRONTEND=noninteractive
17+
18+
RUN apt-get update \
19+
&& apt-get install -y --no-install-recommends \
20+
ca-certificates curl gnupg \
21+
git openssh-client \
22+
vim nano less ripgrep jq \
23+
&& install -d -m 0755 /usr/share/keyrings \
24+
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
25+
| gpg --dearmor -o /usr/share/keyrings/nodesource.gpg \
26+
&& echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
27+
> /etc/apt/sources.list.d/nodesource.list \
28+
&& apt-get update \
29+
&& apt-get install -y --no-install-recommends nodejs \
30+
&& npm install -g @anthropic-ai/claude-code @openai/codex \
31+
&& npm cache clean --force \
32+
&& apt-get clean \
33+
&& rm -rf /var/lib/apt/lists/*
34+
35+
# A login hint so members know the AI tools are present and BYO-key.
36+
RUN printf '%s\n' \
37+
'AgentBBS pod — coding tools ready:' \
38+
' claude (Claude Code) — export ANTHROPIC_API_KEY=... or run: claude' \
39+
' codex (OpenAI Codex) — export OPENAI_API_KEY=... or run: codex' \
40+
' git push → git@git.profullstack.com (your BBS SSH key is your git key)' \
41+
> /etc/motd \
42+
&& printf '[ -n "$PS1" ] && [ -r /etc/motd ] && cat /etc/motd\n' \
43+
> /etc/profile.d/10-agentbbs-motd.sh
44+
45+
CMD ["sleep", "infinity"]

setup.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,21 @@ fi
216216
sudo -u "$SVC_USER" XDG_RUNTIME_DIR="/run/user/$SVC_UID" \
217217
podman pull -q "$POD_IMAGE" >/dev/null 2>&1 || warn "could not pre-pull $POD_IMAGE (pods will pull on first use)"
218218

219+
# Build the member pod image (FROM $POD_IMAGE): adds git, openssh-client, Node,
220+
# and the Claude Code + Codex CLIs so members can code in their pod (BYO API
221+
# key). podman layer-caches, so an unchanged Containerfile rebuilds cheaply. On
222+
# failure we keep the base image rather than break pod launches.
223+
if [ -f "$SRC_DIR/pods/Containerfile" ]; then
224+
log "building member pod image (localhost/agentbbs-pod:latest)"
225+
if sudo -u "$SVC_USER" XDG_RUNTIME_DIR="/run/user/$SVC_UID" \
226+
podman build -t localhost/agentbbs-pod:latest \
227+
-f "$SRC_DIR/pods/Containerfile" "$SRC_DIR/pods" >/dev/null 2>&1; then
228+
POD_IMAGE="localhost/agentbbs-pod:latest"
229+
else
230+
warn "pod image build failed — keeping $POD_IMAGE (run: podman build -f $SRC_DIR/pods/Containerfile $SRC_DIR/pods)"
231+
fi
232+
fi
233+
219234
# ---- 6. environment file ---------------------------------------------------
220235
ENV_DIR=/etc/agentbbs
221236
install -d -m 0750 "$ENV_DIR"
@@ -332,6 +347,9 @@ upsert_env() { # KEY VALUE — skips when VALUE is empty
332347
chmod 0640 "$file"
333348
}
334349
# CoinPay: API key (read by the coinpay CLI) + merchant/business id.
350+
# Point existing installs at the freshly built member pod image (fresh installs
351+
# get it from the env-file template below).
352+
upsert_env AGENTBBS_POD_IMAGE "$POD_IMAGE"
335353
upsert_env COINPAY_API_KEY "${COINPAY_API_KEY:-}"
336354
upsert_env AGENTBBS_COINPAY_MERCHANT_ID "${COINPAY_MERCHANT_ID:-${AGENTBBS_COINPAY_MERCHANT_ID:-}}"
337355
upsert_env COINPAY_BUSINESS_ID "${COINPAY_MERCHANT_ID:-${AGENTBBS_COINPAY_MERCHANT_ID:-}}"

0 commit comments

Comments
 (0)