Development
Everything runs in containers. The host needs Docker, Make, Bash, curl and Git; no Python, pip or uv. The Makefile is the entry point, and make help lists every target.
The dev container
Dockerfile.dev builds psyb0t/hybrids3-dev:latest with the full locked environment from uv.lock, runtime and dev dependencies together. Make targets run it with the repository mounted at /work, as your UID, with no Docker socket. It rebuilds when pyproject.toml or uv.lock change.
make dev-image # build it (other targets do this for you)
make shell # interactive shell inside it
Lint and format
make lint # flake8 app tests, then mypy app
make format # autoflake, isort and black on app and tests
black, isort and flake8 use a line length of 100. black, isort and mypy read their settings from pyproject.toml; flake8 reads .flake8.
Tests
make test-unit # pytest tests/test_lock_unit.py in the dev container
make test # production image, unit tests, then the integration suite
make test is what CI runs. The integration suite, test.sh, builds psyb0t/hybrids3:latest-test, starts it with a test configuration, runs every test against it by container IP and removes it again. The proxy tests start an extra hybrids3 container with a path_prefix and an nginx container in front of it. Output also goes to test.log.
The tests are Bash functions in tests/, one file per area: HTTP, S3, MCP, auth, expiry, edge cases, concurrency, locks, proxy and security. Python snippets inside them (boto3, the FastMCP client, JSON parsing) run through a py helper. With TEST_PYTHON_IMAGE set, which make test does, the helper runs them in the dev image; without it, it uses the host’s python3, which then needs boto3 and fastmcp.
Run single tests after building the dev image:
make dev-image
./test.sh --help
TEST_PYTHON_IMAGE=psyb0t/hybrids3-dev:latest ./test.sh test_s3_presigned_private
A new test is a test_* function in a tests/test_*.sh file followed by ALL_TESTS+=(test_name).
Running a local build
cp config.example.yaml config.yaml # then change the keys
make build
make run # container "hybrids3" on port 8080
make logs
make stop
config.yaml is gitignored.
Dependencies
uv manages dependencies with a hash-locked uv.lock and a supply-chain age gate: [tool.uv] exclude-newer in pyproject.toml refuses any package version published after a fixed date. Every dependency change moves that date to three days ago in the same commit, through scripts/bump_exclude_newer.sh, then relocks.
| Target | What it does | Moves the gate? |
|---|---|---|
make pkg-lock | Relock under the current gate | No |
make pkg-add PKG=name==1.2.3 | Add a package | Yes |
make pkg-remove PKG=name | Remove a package | Yes |
make pkg-update PKG=name | Upgrade one package | Yes |
make pkg-upgrade | Upgrade every package | Yes |
Do not run uv by hand. Every supported change has a target, so the gate and the lockfile always move together.
Production image
make build builds psyb0t/hybrids3:latest from the multi-stage Dockerfile. The builder stage installs locked runtime dependencies with uv sync --frozen --no-dev; the runtime stage copies only that virtual environment and app/ onto the same digest-pinned Python base, adds libmagic1 and runs as UID 1000. There is no pip install anywhere.
Documentation
The site you are reading is docs/, built by GitHub Pages with Jekyll and the Just the Docs theme. Build it locally with the same image GitHub uses:
make docs # build the site into a gitignored output directory
CI
.github/workflows/pipeline.yml runs on every push, using the shared workflows in psyb0t/reusable-github-workflows:
| Push | Jobs |
|---|---|
| Any branch | code: make lint and make test |
main | code, then the image build for linux/amd64 and linux/arm64, pushed as latest and sha-<commit> with a vulnerability scan, then the badges |
vX.Y.Z tag | wait for the main run of the tagged commit to pass, docker-promote the sha-<commit> image to the release tag without rebuilding, release the GitHub release, then publish the skill and plugin to ClawHub and the entry to the MCP Registry |
The other workflows mirror the repository to GitLab and Codeberg, archive it, copy issues back from the mirrors and close pull requests.
Releases
A release is an annotated vX.Y.Z tag on a main commit whose pipeline run passed. Write the CHANGELOG entry and update the version in .agents/.codex-plugin/plugin.json in the commit before tagging. The tag pipeline promotes that commit’s image, so what was tested on main is what ships.