* fix(example): keep agents usable when DSH's inner sandbox has no backend DSH confines bash commands itself (bubblewrap, else Landlock) and fails closed with SANDBOX_UNAVAILABLE when no runner is usable. In this hardened pod - non-root, capabilities dropped, seccomp RuntimeDefault - bubblewrap cannot start (it needs CAP_SYS_ADMIN) and the pod exposes no Landlock runner, so the shipped workspace-write default made every command fail: sandbox mode "workspace-write" is requested but no sandbox backend is usable The DSH base bundle at the pinned ref documents DSH_PERMISSION_MODE as the explicit deployment override, so set it to danger-full-access in both sandbox templates. The per-user pod is the isolation boundary in this example (its own pod, PVC and Service; non-root; all capabilities dropped), so a working agent is worth more than an inner file policy that cannot be enforced. Note that danger-full-access also turns approval prompts off. Trade-off documented rather than hidden: an agent command can now read its own DSH_HOME (including .credentials.yaml) and write anywhere in the container. Hosts where a runner works should keep workspace-write or read-only. - main-image docs: DSH_PERMISSION_MODE row plus a note on the runners and the fail-closed behavior - example README: limits bullet and a troubleshooting row with the in-pod backend check and how to restore a confined mode - architecture: tenant-isolation bullet extended - skill runbook/troubleshooting: operation guidance; never grant CAP_SYS_ADMIN or seccompProfile: Unconfined to "fix" this without an explicit decision Verified: make check (14 manifests), make lint, YAML assertions on both templates, and the override expression at the pinned dsh-v0.2.1-rc.5 base bundle (mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'). * fix(image): install bubblewrap for DSH's command sandbox DSH's Linux sandbox runner chain is bubblewrap, then Landlock, and it fails closed with SANDBOX_UNAVAILABLE when neither works. The image shipped neither, so a sandboxed agent had no way to execute confined commands. Add bubblewrap to the runtime stage. Debian ships it unsetuid (0.12.0), so it still needs unprivileged user namespaces plus the namespace/mount syscalls: in a hardened pod (all capabilities dropped, RuntimeDefault seccomp) it usually cannot create one, and DSH then falls back to Landlock or, failing that, to the DSH_PERMISSION_MODE=danger-full-access override the templates already set. Where the host allows an unprivileged user namespace, the first-choice runner now exists, so the confined modes can be restored. Docs corrected to match: bubblewrap is no longer "absent"; the blocker is the unsetuid binary's namespace requirement, not a missing CAP_SYS_ADMIN grant. Verified: make check, make lint; the package exists in the pinned base (node:24-trixie-slim, bubblewrap 0.12.0-1~deb13u1); a throwaway container with the pod's flags (--user 1000 --cap-drop ALL --security-opt no-new-privileges) still cannot create a namespace, which is why the override stays. No local image build was run. * ci(image): build and smoke-test the main image on PRs that touch it The Dockerfile and the pins in versions.env could only be validated by a merge to main or by running `make image` by hand; neither gives a PR-time signal. Add ci-image.yaml: on pull requests that change Dockerfile, .dockerignore or versions.env (and on manual dispatch) it builds linux/amd64 natively with the pins sourced from versions.env - no registry login, no push - loads the image locally and smoke-tests it: - the packaged CLI runs and reports the pinned version - the container is the non-root node user (1000) and owns /workspace + DSH_HOME - the bubblewrap command-sandbox runner is present - the OCI version label matches versions.env - a container serves /favicon.svg (the same unauthenticated probe the shipped manifests use), with logs filtered so the launch token is never printed Path-filtered like the other on-demand checks, so it is not a required status check; the published multi-arch build stays in build-main.yaml and release.yaml. Docs updated in the same change: AGENTS.md repository map and testing table, docs/gitflow.md checks section, docs/main-image.md build section. Verified: actionlint 1.7.12 and yamllint 1.38.0 (CI versions) clean over the whole repository, plus make check and make lint. --------- Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
7.8 KiB
Gitflow: branches, pull requests and CI
This repository uses trunk-based development with release branches. main is
always releasable; feature work happens on short-lived branches; releases are cut
from main onto a release branch and tagged with an immutable release identity.
See versioning for what the tags mean.
Branches
| Branch | Branched from | Merged into | Purpose |
|---|---|---|---|
main |
— | — | The integration trunk. Protected, linear history, always releasable. |
feat/<slug> |
main |
main (squash) |
New capability or example. |
fix/<slug> |
main |
main (squash) |
Bug fix. |
docs/<slug> |
main |
main (squash) |
Documentation only. |
refactor/<slug>, chore/<slug>, test/<slug> |
main |
main (squash) |
Non-behavioral rework, maintenance, tests. |
release/<identity> |
main at the release commit |
— | Release stabilization and tagging. Only release-blocking fixes land here. |
hotfix/<slug> |
The release branch (or the tagged commit) | main and the release branch |
Urgent production fix. |
Keep branches short-lived. Rebase on main rather than merging main into the
branch; the PR is squash-merged, so individual branch commits are never part of
main history.
Pull requests
- One scoped change per PR. Do not mix a version bump with unrelated work, and update the docs the change touches in the same PR.
- Conventional PR title. The title becomes the squash commit message, so it
is linted by the
pr-titlecheck:feat(scope): ...,fix(scope): ...,docs(scope): ...,build: ...,ci: ...,refactor: ...,test: ...,chore: .... Breaking changes usefeat(scope)!: ...or aBREAKING CHANGE:footer. - CI must be green. All required checks below must pass before merge.
- Squash merge only.
mainstays linear and protected; merged PRs are one commit each. - No direct pushes to
mainor to a release branch. Both are protected. - Review for behavior changes. Infrastructure, workflow, release and gateway-contract changes need a maintainer review; trivial docs changes still need the checks.
Required checks
| Check | Workflow / job | What it verifies |
|---|---|---|
lint |
ci.yaml |
scripts/check-versions.sh (version pins and markdown tokens), scripts/check-examples.sh (example layout), shellcheck, actionlint, yamllint, hadolint and documentation links. |
python |
ci-python.yaml |
Gateway ruff check/format and unit tests (pytest with coverage). Runs only when the gateway or this workflow changes. |
manifests |
ci-manifests.yaml |
scripts/check-manifests.py (manifest shape) and a server-side dry-run of every example manifest against the agent-sandbox CRDs. Runs only when manifests, check scripts or versions.env change. |
pr-title |
pr-title.yaml |
Conventional Commit PR title. |
CodeQL |
codeql.yml |
Static analysis for Python and GitHub Actions. |
dependency-review |
ci.yaml |
Dependency changes introduced by the PR (job dependency-review; runs on pull requests only). |
python, manifests and build-amd64 use native on.pull_request.paths
filtering, so PRs that do not touch the gateway, the manifests or the image
definition skip them. The gateway image build and /healthz are no longer
smoke-tested in a dedicated job: /healthz is covered by the gateway unit tests,
the Dockerfile is linted by lint (hadolint), and the multi-arch image build is
published by build-gateway.yaml on merge.
ci-image.yaml (build-amd64) is the PR-side check for the main image: when
Dockerfile, .dockerignore or versions.env change it builds linux/amd64
with the pins from versions.env — no registry login, no push — and smoke-tests
the result (--version, non-root UID, /workspace + DSH_HOME, the
bubblewrap runner, the version label, and /favicon.svg served from a running
container). The published multi-arch build still happens only in
build-main.yaml on merge.
Because a skipped path-filtered check is not a passing check, only the always-on
checks (lint, pr-title, CodeQL, dependency-review) should be enforced as
required status checks; python, manifests and build-amd64 are on-demand
checks that skip when unrelated to the change.
The image build workflows (_build-image.yaml, build-main.yaml,
build-gateway.yaml) and e2e-kind.yaml are not required on every PR: the
image builds run where they are needed, and the kind end-to-end suite is reserved
for release validation and manual dispatch because it is slow.
Releases
Releases are cut from main:
- Confirm
mainis green andmake check,make test,make lintpass. - Bump versions.env if the DSH ref changed, using
make set-version; otherwise keep the sameDSH_VERSIONand bumpRELEASE_REVISION. - Create
release/<identity>from the release commit. - Run
make release-dry-runand review the generated notes. - Create and push the annotated
dsh-*release tag. Therelease.yamlworkflow validates that the tag matches versions.env, builds and pushes the multi-arch images (version + latest), creates the GitHub Release from the git-cliff notes, and opens achore(changelog)PR that refreshesCHANGELOG.mdonmain. - Merge the changelog PR and any release-branch-only fixes back to
main.
Full procedure: versioning.
Hotfixes
A released tag is never moved or re-tagged — tags and image identities are immutable. To fix a broken release:
- Branch
hotfix/<slug>from the release tag (or its release branch). - Make the minimal fix, add a test, open a PR; the same required checks apply.
- Publish a new identity with a higher
RELEASE_REVISION(-r(N+1)). Do not reuse or delete the broken tag. - Merge the fix back into
main(or cherry-pick it) so the trunk is fixed too.
Rollback is also a re-release, never a retag: see versioning.
Tag protection
All release tags match dsh-* and are protected:
- only maintainers (the release workflow) may create them;
- they cannot be deleted or force-updated;
- they are annotated and point at the release commit on a release branch.
Image tags derive from the same identity and are equally immutable; the mutable
latest and main image channels always point at a release or a trunk build
respectively.
One-time GitHub settings checklist
Configure these on the repository before relying on the workflow:
- Branches → main: require a pull request; require the checks
lint,python,manifests,pr-title,CodeQL,dependency-review; require branches to be up to date; require conversation resolution; restrict direct pushes; allow only squash merges and enable "Automatically delete head branches". - Branches → release/*: same protections, plus allow the release workflow to push tags.
- Tags → dsh-*: restrict creation/update/deletion to maintainers; a tag push triggers the release workflow.
- Actions → General: allow GitHub Actions; set workflow permissions to
read-only by default; allow
GITHUB_TOKENto create pull requests only where required. - Actions → Runners: make the arm64 runners (
ubuntu-24.04-arm) available to the public/private repository as appropriate. - Packages: grant the release workflow
packages: write(workflow permissions do this) and protect theghcr.io/sredevopsorg/deepseek-kubernetes-sandboxpackages from deletion. - Security: enable CodeQL default setup or keep the advanced
codeql.ymlworkflow (not both), enable Dependabot alerts and dependency review. - Secrets/Variables: none are needed for image publishing; the workflows
use
GITHUB_TOKEN.