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
1 change: 1 addition & 0 deletions .gavel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ checks: {}
commit:
allow:
- pkg/cli/webapp/dist/.gitkeep
- pkg/cli/webapp/dist/index.html
grouping: {}
lint: {}
message: {}
Expand Down
96 changes: 96 additions & 0 deletions .github/workflows/publish-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Publish Image

# Manual only. The agent sandbox base image (pkg/container/base) is large and
# slow to build, so it is published on demand rather than on every release.
#
# Dispatch this from the tag you want to ship — the reusable workflow builds
# whatever ref you select. Run it *after* the corresponding release exists:
# the image installs flanksource/captain through deps at 'latest', so without
# the release assets it would bake in the previous version's binary.
on:
workflow_dispatch:
inputs:
version:
description: >-
Image tag to publish, e.g. v0.0.27. Defaults to the most recent git
tag reachable from the selected ref.
required: false
type: string
latest:
description: Also publish the :latest tag.
required: false
type: boolean
default: true
platforms:
description: Platforms to build.
required: false
type: string
default: linux/amd64,linux/arm64
build_args:
description: >-
Newline-separated Docker build args overriding the Dockerfile's pinned
defaults, e.g. CLAUDE_CODE_VERSION=2.1.227. Leave empty to publish
exactly what the Dockerfile pins — every version it installs is an ARG,
so a tag republished with no overrides reproduces its contents.
required: false
type: string

permissions:
contents: read

jobs:
resolve:
name: Resolve image tags
runs-on: ubuntu-latest
outputs:
image_tags: ${{ steps.tags.outputs.image_tags }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false

- id: tags
env:
INPUT_VERSION: ${{ inputs.version }}
INPUT_LATEST: ${{ inputs.latest }}
run: |
set -euo pipefail

version=$INPUT_VERSION
if [ -z "$version" ]; then
version=$(git describe --tags --abbrev=0)
echo "No version supplied; using most recent tag: $version"
fi
case "$version" in
*[,[:space:]]*) echo "version must not contain commas or whitespace: $version" >&2; exit 1 ;;
esac

tags=""
for repo in docker.io/flanksource/captain ghcr.io/flanksource/captain; do
tags="$tags$repo:$version,"
if [ "$INPUT_LATEST" = "true" ]; then
tags="$tags$repo:latest,"
fi
done

echo "image_tags=${tags%,}" >> "$GITHUB_OUTPUT"
echo "Publishing: ${tags%,}"

docker:
name: Build and publish
needs: resolve
permissions:
contents: read
id-token: write # keyless cosign signatures
packages: write # push to ghcr.io
uses: flanksource/action-workflows/.github/workflows/publish-multi-platform-docker-image.yml@f8512a65d38c1ea53d5c83a4e30ee2ae56acac6f # v1.2.1
with:
dockerfile: pkg/container/base/Dockerfile
context: pkg/container/base
image_tags: ${{ needs.resolve.outputs.image_tags }}
platforms: ${{ inputs.platforms }}
build_args: ${{ inputs.build_args }}
secrets:
docker_username: ${{ secrets.DOCKER_USERNAME }}
docker_password: ${{ secrets.DOCKER_PASSWORD }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ hack/*
.ok/
.okignore
.ginkgo/
!pkg/cli/webapp/dist/index.html
82 changes: 0 additions & 82 deletions Dockerfile

This file was deleted.

37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,10 @@ captain/
├── pkg/cmux/ # Terminal multiplexer integration (processes and screenshots)
├── pkg/collections/ # Generic collection utilities
├── pkg/container/ # Sandbox discovery, generation, build/run logic
├── pkg/container/base/ # Embedded agent base image (Dockerfile, deps.yaml, entrypoint.sh)
├── pkg/dod/ # Definition of Done persistence and execution
├── pkg/git/ # Git worktree helpers
├── pkg/sandbox/ # Token/preset/sandbox helpers
├── Dockerfile # Container image for captain/Claude tooling
├── entrypoint.sh # gosu-based user switching entrypoint
├── Makefile # Thin wrapper around Taskfile
└── Taskfile.yaml # Main developer tasks
```
Expand Down Expand Up @@ -615,18 +614,42 @@ By default this copies the built binary to:

## Docker image

The included `Dockerfile` builds on `flanksource/base-image` and installs:

- Node.js
- git, gh, jq, vim, nano, zsh, fzf, etc.
- Claude Code via `@anthropic-ai/claude-code`
`pkg/container/base/Dockerfile` (embedded into the binary, built as `claude-env:base` by
`captain container`) builds on `flanksource/base-image` and installs a full agent toolchain:

- **Agent CLIs** — one per backend: `claude` (`@anthropic-ai/claude-code`), `codex`
(`@openai/codex`), `gemini` (`@google/gemini-cli`), plus `tsx` for the `claude-agent`
SDK bridge
- **Flanksource tools** — `captain`, `gavel`, `repomap` (installed via `deps` from
`pkg/container/base/deps.yaml`)
- **Go** — toolchain, `task`, `ginkgo`, `golangci-lint`
- **Node** — Node.js 22, `npm`, `pnpm`, `typescript`
- **Browser automation** — `agent-browser` and Playwright Chromium (shared at
`/ms-playwright`)
- **Shell tooling** — `rg`, `fd`, `bat`, `delta`, `gh`, `jq`, `tree`, `htop`, `lsof`,
`sqlite3`, `psql`, `tmux`, `shellcheck`, `vim`, `nano`, `zsh`, `fzf`, git

The image is set up to:

- create a user matching host UID/GID
- switch execution using `gosu`
- use `/workspace` as the working directory

Building it requires BuildKit. Set `GITHUB_TOKEN` (or `GH_TOKEN`) before
`captain container build` to avoid GitHub's unauthenticated API rate limit — captain passes
it through as a BuildKit secret, so it never lands in image history. To build by hand:

```bash
DOCKER_BUILDKIT=1 docker build -t claude-env:base \
--secret id=GITHUB_TOKEN,env=GITHUB_TOKEN \
pkg/container/base
```

Publishing to `flanksource/captain` on Docker Hub and GHCR (`linux/amd64` +
`linux/arm64`) is the **Publish Image** workflow. It is manual only — dispatch it from
the tag you want to ship, and only once that tag's release exists, since the image
installs `captain` from the latest GitHub release.

## Dependencies and stack

Primary stack:
Expand Down
5 changes: 0 additions & 5 deletions entrypoint.sh

This file was deleted.

8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.42.0
github.com/charmbracelet/huh v1.0.0
github.com/firebase/genkit/go v1.11.0
github.com/flanksource/clicky v1.21.52
github.com/flanksource/clicky/aichat v1.21.48
github.com/flanksource/commons v1.55.0
github.com/flanksource/clicky v1.21.54
github.com/flanksource/clicky/aichat v1.21.54
github.com/flanksource/commons v1.56.0
github.com/flanksource/sandbox-runtime v1.0.2
github.com/fsnotify/fsnotify v1.9.0
github.com/google/dotprompt/go v0.0.0-20260502013637-5cd4a8405ca3
Expand Down Expand Up @@ -40,7 +40,7 @@ require (
)

require (
github.com/flanksource/commons-db v0.1.26
github.com/flanksource/commons-db v0.1.27
github.com/gliderlabs/ssh v0.3.8
github.com/pelletier/go-toml/v2 v2.4.3
)
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,14 @@ github.com/fergusstrange/embedded-postgres v1.34.0 h1:c6RKhPKFsLVU+Tdxsx8q0UxCHs
github.com/fergusstrange/embedded-postgres v1.34.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk=
github.com/firebase/genkit/go v1.11.0 h1:dmf219fDGP6IcKwPkJoTnXpcTEya7o5Bi+z5iP+FuTk=
github.com/firebase/genkit/go v1.11.0/go.mod h1:x9h3TsbMLiuK1hgbwnkUmKQ9bmTNPORHaqG9XQhI0K8=
github.com/flanksource/clicky v1.21.52 h1:JtcBD05mIbE0cLuhu218Z9uO6O2mFARmgrtEIBHCoVE=
github.com/flanksource/clicky v1.21.52/go.mod h1:eonv42hF6W1IPjQXOL6roBm/nHCYNomqc3Kps2E3RZE=
github.com/flanksource/clicky/aichat v1.21.48 h1:f8Kvl96Lfp1qcqPuve1zsjaYN8ZcK1/FYqnkGA7VN30=
github.com/flanksource/clicky/aichat v1.21.48/go.mod h1:PGN/lVAgxpchRctciUCpR4YIuqWoDwRxDh339A6wi3w=
github.com/flanksource/commons v1.55.0 h1:gj9zBY3V1qgAAnEiLaeGbkqCmNK0p1tJVQCDurdTZ2k=
github.com/flanksource/commons v1.55.0/go.mod h1:gupTCRqGpgD8dd2ooE7bMDJxkfcVKvkPVuHg3cWkW+Q=
github.com/flanksource/commons-db v0.1.26 h1:NXAP0WvMs4ufyDfl1L2ryRxBv5qxV67GiI1nINd4YIw=
github.com/flanksource/commons-db v0.1.26/go.mod h1:i378WIxy8g9xOeLBvhh1y3FO99oCumUqNmfhuDF79kM=
github.com/flanksource/clicky v1.21.54 h1:FZUh4GjcbnR6yh7qhdZb9Y1H2TXwlUfIfslMD+EJ14o=
github.com/flanksource/clicky v1.21.54/go.mod h1:eonv42hF6W1IPjQXOL6roBm/nHCYNomqc3Kps2E3RZE=
github.com/flanksource/clicky/aichat v1.21.54 h1:1qCcHfDcN2HrxW6/TqhfbQBNfLAXBXydIh1DkmJ1JJY=
github.com/flanksource/clicky/aichat v1.21.54/go.mod h1:JWR4cNoHQgBYiWE14cPwteWFzflPlJPStWvkY/DFA7E=
github.com/flanksource/commons v1.56.0 h1:/L1eWb3iLDM3UQEgd6vCTlG9Kbl6ssH6S0YR5f026IY=
github.com/flanksource/commons v1.56.0/go.mod h1:gupTCRqGpgD8dd2ooE7bMDJxkfcVKvkPVuHg3cWkW+Q=
github.com/flanksource/commons-db v0.1.27 h1:F6g5EY2pbK1qO/S6iewuk+VvYw7rdfp0xH9aiFP2AEU=
github.com/flanksource/commons-db v0.1.27/go.mod h1:7zw8o/HLP/sTTAzb+7NHa50Iy8FIOevUjan+900MnBc=
github.com/flanksource/gomplate/v3 v3.24.84 h1:UOE0yCJsczTIKRaHUvhD6tjCYrbNvOugAizuy0FVlhE=
github.com/flanksource/gomplate/v3 v3.24.84/go.mod h1:NMMZkFsjbLy/8iY8Fip5N86Y0PP6lZeq+kmPwpVVIL0=
github.com/flanksource/is-healthy v1.0.88 h1:ATQuKoNdp8Qfzf41/eMFajmT0qzOmZlZNG5eLK41RFo=
Expand Down
Loading
Loading