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
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ ANTHROPIC_API_KEY=
# PAPERCLIP_DEPLOYMENT_MODE=authenticated
# PAPERCLIP_BIND=lan # Docker/Podman-friendly bind preset
# PAPERCLIP_ALLOWED_HOSTNAMES=192.168.1.50,my-host.local # Trusted LAN/private hosts only, no scheme or port
# ENABLE_HERMES=true # Start Hermes API server on HERMES_PORT
# HERMES_PORT=8642
# Bundled Hermes is temporarily unavailable in v1.1.3. Existing
# /home/opencode/.hermes data remains preserved for rollback or a future fix.
# CLIPROXYAPI_ENABLED=true # Add OpenCode provider for an externally managed CLIProxyAPI endpoint
# CLIPROXYAPI_BASE_URL=http://your-cliproxy-host:8317/v1
# CLIPROXYAPI_API_KEY=
Expand Down
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.sh text eol=lf
*.py text eol=lf
s6-overlay/**/run text eol=lf
s6-overlay/**/type text eol=lf
6 changes: 4 additions & 2 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ Release tags use exact `vX.Y.Z`; Docker image tags drop the `v` prefix. Each seg

HolyCode ships many third-party CLIs inside one Docker image. Tagged releases refresh the pinned Dockerfile tools, but optional OpenCode plugins are live registry installs trusted at container startup when you enable them. Those boot-installed plugins are outside the image SBOM. Renovate configuration covers images, Actions, npm, PyPI, GitHub releases, and plugin pins; release audits still record scanner findings and compatibility holds before publication.

Trivy's release gate keeps its full critical/high report visible and blocks fixable critical findings. npm 12 lifecycle scripts are denied by default and checked against exact package versions, script bodies, and architecture rules. The dated versions, holds, removals, and scanner disposition are recorded in [`docs/dependency-audit-v1.1.2.md`](../docs/dependency-audit-v1.1.2.md).
Trivy's release gate keeps its full critical/high report visible and blocks every fixable critical or high finding. npm lifecycle scripts are disabled during installation, then checked against exact package versions, integrity values, script bodies, and architecture rules before approved scripts run. The dated versions, holds, removals, and scanner disposition are recorded in [`docs/dependency-audit-v1.1.3.md`](../docs/dependency-audit-v1.1.3.md).

When `ENABLE_PAPERCLIP=true`, HolyCode exposes an authenticated local agent board on the configured Paperclip port. Keep that port on trusted LAN/private networks or behind a VPN, and do not publish it directly to the public internet.

When `ENABLE_HERMES=true`, set `API_SERVER_KEY` and keep port `8642` on a trusted network. If you enable CLIProxyAPI integration, keep the external endpoint private and protect its config, auth files, and API key outside HolyCode.
Bundled Hermes is temporarily unavailable in v1.1.3 because its current releases require vulnerable dependency pins. HolyCode preserves `/home/opencode/.hermes`, and `ENABLE_HERMES=true` stops startup with a migration message. If you enable CLIProxyAPI integration, keep the external endpoint private and protect its config, auth files, and API key outside HolyCode.

Chromium runs as the `opencode` user with its sandbox enabled. Keep `config/chromium-seccomp.json` attached through Compose or the equivalent Podman `--security-opt` option. Do not work around browser failures with `--no-sandbox`, `SYS_ADMIN`, or `seccomp=unconfined`.

Netlify CLI is installed without its platform-specific local functions binaries. HolyCode supports remote build/deploy commands only; `netlify dev` and local functions are intentionally unsupported until upstream rebuilds those binaries.
190 changes: 134 additions & 56 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:
- 'v*'

concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: true
group: docker-release
cancel-in-progress: false

env:
DOCKERHUB_IMAGE: coderluii/holycode
Expand All @@ -19,58 +19,95 @@ env:
# DOCKERHUB_TOKEN - Docker Hub access token with push and description-update access.

jobs:
protected-validation:
uses: ./.github/workflows/protected-validation.yml
secrets: inherit
with:
version: ${{ github.ref_name }}
previous_version: v1.1.1
previous_image: coderluii/holycode:1.1.1@sha256:7d64d2cbfa55673b0b98529b38ec11df8e69354e9c36976139ea14132c98a8fd
release_title: ${{ github.ref_name }}
allow_legacy_bridge: false

build-and-push:
needs: protected-validation
verify-protected-validation:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
run_id: ${{ steps.protected.outputs.run_id }}

steps:
- name: Require successful protected validation for this commit
id: protected
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

response="$(gh api \
-H 'X-GitHub-Api-Version: 2026-03-10' \
"/repos/$GITHUB_REPOSITORY/actions/workflows/protected-validation.yml/runs?head_sha=$GITHUB_SHA&status=success&event=workflow_dispatch&per_page=100")"
run_id="$(jq -er --arg sha "$GITHUB_SHA" \
'[.workflow_runs[] | select(.head_sha == $sha and .conclusion == "success")] | sort_by(.created_at) | last | .id' \
<<<"$response")"
echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
echo "Protected validation run: $run_id" >> "$GITHUB_STEP_SUMMARY"

promote-candidate:
needs: verify-protected-validation
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Validate release tag and commit title
run: |
set -euo pipefail

current="${GITHUB_REF_NAME}"
current="$GITHUB_REF_NAME"
previous="$(git tag --list 'v*' --sort=-v:refname | grep -Fxv "$current" | head -n 1 || true)"
commit_title="$(git log -1 --format=%s "$GITHUB_SHA")"
legacy_args=()

if [ "$previous" = "v1.0.13" ] && [ "$current" = "v1.1.0" ]; then
legacy_args=(--allow-legacy-v1-0-13-to-v1-1-0)
fi

python scripts/validate_version.py \
--version "$current" \
--previous-version "$previous" \
--tag-name "$current" \
--commit-title "$commit_title" \
"${legacy_args[@]}"
--commit-title "$commit_title"

- name: Require matching published GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

- name: Free up disk space
release=''
for _ in $(seq 1 18); do
if release="$(gh api \
-H 'X-GitHub-Api-Version: 2026-03-10' \
"/repos/$GITHUB_REPOSITORY/releases/tags/$GITHUB_REF_NAME" 2>/dev/null)"; then
break
fi
sleep 10
done
[ -n "$release" ]
jq -e --arg tag "$GITHUB_REF_NAME" '
.tag_name == $tag and
.name == $tag and
.draft == false and
(.body | test("^## [0-9]{2}/[0-9]{2}/[0-9]{4}"))
' <<<"$release" >/dev/null

- name: Download protected candidate digest
env:
GH_TOKEN: ${{ github.token }}
PROTECTED_RUN_ID: ${{ needs.verify-protected-validation.outputs.run_id }}
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache
sudo apt-get clean
docker system prune -af
df -h
set -euo pipefail

- name: Set up QEMU (multi-arch)
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
gh run download "$PROTECTED_RUN_ID" \
--repo "$GITHUB_REPOSITORY" \
--name "holycode-$GITHUB_REF_NAME-candidate" \
--dir candidate
candidate_digest="$(tr -d '\r\n' < candidate/candidate-digest.txt)"
[[ "$candidate_digest" =~ ^sha256:[0-9a-f]{64}$ ]]
echo "CANDIDATE_DIGEST=$candidate_digest" >> "$GITHUB_ENV"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
Expand All @@ -88,39 +125,80 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
${{ env.DOCKERHUB_IMAGE }}
${{ env.GHCR_IMAGE }}
tags: |
type=raw,value=latest
type=semver,pattern={{version}}

- name: Build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
sbom: true
provenance: mode=max
- name: Verify and promote exact protected candidate
run: |
set -euo pipefail

version="${GITHUB_REF_NAME#v}"
source="$GHCR_IMAGE@$CANDIDATE_DIGEST"
manifest="$(docker buildx imagetools inspect "$source" --format '{{json .Manifest}}')"
digest="$(jq -er '.digest' <<<"$manifest")"
platforms="$(jq '[.manifests[] | select(.platform.os == "linux" and (.platform.architecture == "amd64" or .platform.architecture == "arm64"))] | length' <<<"$manifest")"
attestations="$(jq '[.manifests[] | select(.annotations["vnd.docker.reference.type"] == "attestation-manifest")] | length' <<<"$manifest")"
[ "$digest" = "$CANDIDATE_DIGEST" ]
[ "$platforms" -eq 2 ]
[ "$attestations" -ge 2 ]

docker buildx imagetools create \
--tag "$DOCKERHUB_IMAGE:$version" \
--tag "$DOCKERHUB_IMAGE:latest" \
--tag "$GHCR_IMAGE:$version" \
--tag "$GHCR_IMAGE:latest" \
"$source"

- name: Verify published manifests and attestations
run: |
set -euo pipefail

version="${GITHUB_REF_NAME#v}"
refs=(
"$DOCKERHUB_IMAGE:$version"
"$DOCKERHUB_IMAGE:latest"
"$GHCR_IMAGE:$version"
"$GHCR_IMAGE:latest"
)

for attempt in $(seq 1 12); do
ready=true
for ref in "${refs[@]}"; do
if ! docker buildx imagetools inspect "$ref" >/dev/null 2>&1; then
ready=false
break
fi
done
[ "$ready" = true ] && break
sleep 10
done
[ "$ready" = true ]

expected_digest=''
for ref in "${refs[@]}"; do
manifest="$(docker buildx imagetools inspect "$ref" --format '{{json .Manifest}}')"
digest="$(jq -er '.digest' <<<"$manifest")"
platforms="$(jq '[.manifests[] | select(.platform.os == "linux" and (.platform.architecture == "amd64" or .platform.architecture == "arm64"))] | length' <<<"$manifest")"
attestations="$(jq '[.manifests[] | select(.annotations["vnd.docker.reference.type"] == "attestation-manifest")] | length' <<<"$manifest")"
[ "$platforms" -eq 2 ]
[ "$attestations" -ge 2 ]
if [ -z "$expected_digest" ]; then
expected_digest="$digest"
else
[ "$digest" = "$expected_digest" ]
fi
echo "$ref -> $digest ($platforms platforms, $attestations attestation manifests)"
done

[ "$expected_digest" = "$CANDIDATE_DIGEST" ]
echo "Published multi-architecture digest: $expected_digest" >> "$GITHUB_STEP_SUMMARY"

update-dockerhub-description:
needs: build-and-push
needs: promote-candidate
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Update Docker Hub description
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
Expand Down
64 changes: 21 additions & 43 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Python version policy tests
run: python -m unittest discover -s tests

- name: Static action pin checks
run: python scripts/validate_workflow_pins.py

- name: Chromium seccomp profile
run: python scripts/validate_chromium_seccomp.py

- name: JSON syntax
run: python -m json.tool renovate.json >/tmp/renovate.json

Expand All @@ -40,7 +43,7 @@ jobs:
fi

- name: Renovate configuration
run: npx --yes --package renovate@43.263.9 renovate-config-validator --strict renovate.json
run: npx --yes --package renovate@43.274.0 renovate-config-validator --strict renovate.json

build-and-smoke-test:
needs: static-validation
Expand All @@ -50,7 +53,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Build image
run: docker build -t holycode-pr-test .
Expand Down Expand Up @@ -115,10 +118,10 @@ jobs:
done
[ "$ready" = true ]

docker exec holycode-paperclip-test sh -lc '
pid="$(pgrep -u opencode -f "/usr/local/bin/paperclipai" | head -n 1)"
docker exec -u opencode holycode-paperclip-test sh -lc '
pid="$(pgrep -u opencode -f "^node.*/paperclipai" | head -n 1)"
test -n "$pid"
tr "\0" "\n" < "/proc/$pid/environ" > /tmp/paperclip-env
xargs -0 -n 1 < "/proc/$pid/environ" > /tmp/paperclip-env
grep -Fx "HOME=/home/opencode" /tmp/paperclip-env
grep -Fx "XDG_CONFIG_HOME=/home/opencode/.config" /tmp/paperclip-env
grep -Fx "XDG_CACHE_HOME=/home/opencode/.cache" /tmp/paperclip-env
Expand All @@ -137,46 +140,21 @@ jobs:
exit 1
fi

- name: Smoke test Hermes runtime environment
- name: Validate Hermes migration guard
run: |
set -eu

docker run -d --name holycode-hermes-test \
-e ENABLE_HERMES=true \
-e API_SERVER_KEY=holycode-ci-smoke-key \
holycode-pr-test

cleanup() {
docker logs holycode-hermes-test || true
docker rm -f holycode-hermes-test || true
}
trap cleanup EXIT

ready=false
for _ in $(seq 1 90); do
if docker exec holycode-hermes-test curl -fsS \
-H 'Authorization: Bearer holycode-ci-smoke-key' \
http://localhost:8642/v1/models >/dev/null 2>&1; then
ready=true
break
fi
sleep 2
done
[ "$ready" = true ]

docker exec -u opencode holycode-hermes-test sh -lc '
pid="$(pgrep -u opencode -f "hermes gateway" | head -n 1)"
test -n "$pid"
tr "\0" "\n" < "/proc/$pid/environ" > /tmp/hermes-env
grep -Fx "HOME=/home/opencode" /tmp/hermes-env
grep -Fx "XDG_CONFIG_HOME=/home/opencode/.config" /tmp/hermes-env
grep -Fx "XDG_CACHE_HOME=/home/opencode/.cache" /tmp/hermes-env
grep -Fx "XDG_DATA_HOME=/home/opencode/.local/share" /tmp/hermes-env
grep -Fx "XDG_STATE_HOME=/home/opencode/.local/state" /tmp/hermes-env
test -w /home/opencode/.hermes
'
docker volume create holycode-hermes-data >/dev/null
trap 'docker volume rm holycode-hermes-data >/dev/null 2>&1 || true' EXIT
docker run --rm --entrypoint sh -v holycode-hermes-data:/home/opencode holycode-pr-test \
-lc 'mkdir -p /home/opencode/.hermes && touch /home/opencode/.hermes/preserved'

docker logs holycode-hermes-test > /tmp/holycode-hermes.log 2>&1 || true
if grep -E '/root/\.(config|cache|local)|EACCES.*(/root|/home/opencode)' /tmp/holycode-hermes.log; then
if docker run --rm -v holycode-hermes-data:/home/opencode \
-e ENABLE_HERMES=true holycode-pr-test >/tmp/holycode-hermes.log 2>&1; then
echo "ENABLE_HERMES=true unexpectedly started the removed bundled service" >&2
exit 1
fi
grep -F "bundled Hermes is temporarily unavailable" /tmp/holycode-hermes.log
grep -F "/home/opencode/.hermes is preserved" /tmp/holycode-hermes.log
docker run --rm --entrypoint test -v holycode-hermes-data:/home/opencode \
holycode-pr-test -f /home/opencode/.hermes/preserved
Loading