From 3eeae3ee8634b229784c638534046834f4a6e0cd Mon Sep 17 00:00:00 2001 From: Marketen Date: Wed, 9 Sep 2026 15:51:42 +0200 Subject: [PATCH 1/2] Resolve the SDK commit at build time instead of maintaining a pin The pin was not protecting anything. Nothing measures or attests the proxy image -- there is no PCR for it, no verify-build script, no point at which anyone checks the running binary against source. The package is trusted because Dappnode publishes it onchain, and both repositories are open source. So the pin bought a provenance record, at the cost of a PR for every SDK change, and the SDK has changed ten times since 17 August. CI now resolves dappnode-nexus-sdk main to a commit and stamps it into the manifest and compose before building. The record survives -- the published package still says exactly which SDK source went into it -- but it is an output rather than something a human copies. Nothing about the build gets weaker. The Dockerfile still demands a full 40-character SHA and re-checks it after fetching, so the build stays content-addressed; only who supplies the value changed. Base images were already pinned by digest. Two things fall out of it: - The NEXUS_SDK_TOKEN machinery goes. dappnode-nexus-sdk is public, the workflow no longer passes the secret, and leaving a dead secret mount in the Dockerfile and compose would just be confusing. This supersedes #14. - The release job gains `needs: build-test`. It had none, so a failing build test did not stop a publish -- open since the 24 August session notes. Verified: the script resolves main, stamps both files, and the image builds from the result with no credentials. Co-Authored-By: Claude Opus 5 --- .github/workflows/main.yml | 14 +++++++------ Dockerfile | 8 +------- dappnode_package.json | 2 +- docker-compose.yml | 8 +------- scripts/resolve-sdk-commit.sh | 37 +++++++++++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 21 deletions(-) create mode 100755 scripts/resolve-sdk-commit.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ec6e06..5370a40 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,22 +14,24 @@ jobs: name: Build test steps: - uses: actions/checkout@v4 + - name: Resolve the SDK commit to build + run: ./scripts/resolve-sdk-commit.sh - run: npx @dappnode/dappnodesdk build --skip_save - env: - # Read access to the private dappnode-nexus-sdk repository. The - # default GITHUB_TOKEN is scoped to this repository only and cannot - # fetch the proxy source. - NEXUS_SDK_TOKEN: ${{ secrets.NEXUS_SDK_TOKEN }} release: name: Release + needs: build-test runs-on: ubuntu-latest if: github.event_name == 'push' || github.event_name == 'repository_dispatch' steps: - uses: actions/checkout@v4 + # Resolves dappnode-nexus-sdk main to the commit this build actually used + # and records it in the manifest, so the published package says which SDK + # source went into it without anyone maintaining the value by hand. + - name: Resolve the SDK commit to build + run: ./scripts/resolve-sdk-commit.sh - name: Publish run: npx @dappnode/dappnodesdk publish patch --dappnode_team_preset --timeout 2h env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NEXUS_SDK_TOKEN: ${{ secrets.NEXUS_SDK_TOKEN }} DEVELOPER_ADDRESS: "0xf35960302a07022aba880dffaec2fdd64d5bf1c1" diff --git a/Dockerfile b/Dockerfile index 1f7196e..2f5b53d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,13 +15,7 @@ RUN apk add --no-cache ca-certificates git WORKDIR /src RUN git init \ && git remote add origin https://github.com/dappnode/dappnode-nexus-sdk.git -RUN --mount=type=secret,id=github_token \ - if test -s /run/secrets/github_token; then \ - auth_header="$(printf 'x-access-token:%s' "$(cat /run/secrets/github_token)" | base64 | tr -d '\n')"; \ - git -c http.extraHeader="Authorization: Basic ${auth_header}" fetch --depth=1 origin "${UPSTREAM_VERSION}"; \ - else \ - git fetch --depth=1 origin "${UPSTREAM_VERSION}"; \ - fi \ +RUN git fetch --depth=1 origin "${UPSTREAM_VERSION}" \ && git checkout --detach FETCH_HEAD \ && test "$(git rev-parse HEAD)" = "${UPSTREAM_VERSION}" diff --git a/dappnode_package.json b/dappnode_package.json index 14bb879..7aa3510 100644 --- a/dappnode_package.json +++ b/dappnode_package.json @@ -1,7 +1,7 @@ { "name": "nexus-proxy.dnp.dappnode.eth", "version": "0.1.0", - "upstreamVersion": "219977388b9f26435c2529fc1cd52ff7229e5397", + "upstreamVersion": "39a2ccee325148b011c959de5118f355652ead52", "upstreamRepo": "dappnode/dappnode-nexus-sdk", "upstreamArg": "UPSTREAM_VERSION", "shortDescription": "Keep your Nexus prompts private, and verify it yourself", diff --git a/docker-compose.yml b/docker-compose.yml index 0fd65ef..edc1dee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,9 +6,7 @@ services: context: . dockerfile: Dockerfile args: - UPSTREAM_VERSION: 219977388b9f26435c2529fc1cd52ff7229e5397 - secrets: - - github_token + UPSTREAM_VERSION: 39a2ccee325148b011c959de5118f355652ead52 image: nexus-proxy.dnp.dappnode.eth:0.1.0 container_name: DAppNodeCore-nexus-proxy.dnp.dappnode.eth restart: unless-stopped @@ -49,7 +47,3 @@ services: volumes: verification_state: {} - -secrets: - github_token: - environment: NEXUS_SDK_TOKEN diff --git a/scripts/resolve-sdk-commit.sh b/scripts/resolve-sdk-commit.sh new file mode 100755 index 0000000..7100664 --- /dev/null +++ b/scripts/resolve-sdk-commit.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Resolve dappnode-nexus-sdk to the commit this build will use, and write it +# into the manifest and compose so the published package records its own +# provenance. +# +# This value is an output, not an input. Nothing verifies the proxy image +# against it -- the package is trusted because Dappnode published it onchain -- +# so hand-maintaining a pin bought a record that CI can produce for free, at +# the cost of a bump PR for every SDK change. +# +# The Dockerfile still requires a full 40-character commit SHA and re-checks it +# after fetching, so the build remains content-addressed. Only who supplies the +# value changed. +set -eu + +REPO="${SDK_REPO:-https://github.com/dappnode/dappnode-nexus-sdk.git}" +REF="${SDK_REF:-main}" + +COMMIT="$(git ls-remote "$REPO" "refs/heads/$REF" | cut -f1)" +if ! printf '%s' "$COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "could not resolve $REPO $REF to a commit (got: '$COMMIT')" >&2 + exit 1 +fi + +echo "SDK $REF resolves to $COMMIT" + +# Both files carry it: the manifest is what the published package shows, the +# compose build-arg is what the image is actually built from. +tmp="$(mktemp)" +sed -E "s/\"upstreamVersion\": \"[0-9a-f]{40}\"/\"upstreamVersion\": \"$COMMIT\"/" \ + dappnode_package.json > "$tmp" && mv "$tmp" dappnode_package.json +tmp="$(mktemp)" +sed -E "s/UPSTREAM_VERSION: [0-9a-f]{40}/UPSTREAM_VERSION: $COMMIT/" \ + docker-compose.yml > "$tmp" && mv "$tmp" docker-compose.yml + +grep -q "$COMMIT" dappnode_package.json && grep -q "$COMMIT" docker-compose.yml \ + || { echo "failed to stamp the resolved commit" >&2; exit 1; } From 90e2fa0e3dcd5219f9512f18d6007c7912e54c69 Mon Sep 17 00:00:00 2001 From: Marketen Date: Wed, 9 Sep 2026 15:59:14 +0200 Subject: [PATCH 2/2] Track SDK releases instead of a commit pin Replaces the hand-maintained SHA with the pattern every other Dappnode package already uses: upstreamVersion names an upstream release, and the bump bot opens the PR. Hermes tracks NousResearch/hermes-agent exactly this way. The SDK had nothing to point at, which is the only reason a SHA was there in the first place; dappnode/dappnode-nexus-sdk#11 makes it cut a release on every merge to main. - upstreamVersion becomes v0.1.0 and the Dockerfile takes a release tag, validating the shape and fetching refs/tags/. - auto_check.yml calls dappnode/workflows bump-upstream, same as Hermes, so a new SDK release arrives as a PR rather than someone noticing. - NEXUS_SDK_TOKEN goes: the SDK is public, so the secret mount in the Dockerfile, the compose secrets block and both workflow env entries were all dead. Supersedes #14. - The release job gains needs: build-test. It had none, so a failing build test did not stop a publish. Open since the 24 August notes. Verified: the tag shape check accepts v0.1.0 and rejects a SHA, a bare branch name and a two-part version; fetching refs/tags/ resolves against a repository that has tags; manifest and compose pass dappmanager validation. The image itself cannot build until the first SDK release exists. Co-Authored-By: Claude Opus 5 --- .github/workflows/auto_check.yml | 14 ++++++++++++ .github/workflows/main.yml | 7 ------ Dockerfile | 8 +++---- dappnode_package.json | 2 +- docker-compose.yml | 2 +- scripts/resolve-sdk-commit.sh | 37 -------------------------------- 6 files changed, 19 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/auto_check.yml delete mode 100755 scripts/resolve-sdk-commit.sh diff --git a/.github/workflows/auto_check.yml b/.github/workflows/auto_check.yml new file mode 100644 index 0000000..aaa0bc8 --- /dev/null +++ b/.github/workflows/auto_check.yml @@ -0,0 +1,14 @@ +name: Bump upstream version + +on: + schedule: + - cron: "00 */4 * * *" + workflow_dispatch: + push: + branches: + - "main" + +jobs: + bump-upstream: + uses: dappnode/workflows/.github/workflows/bump-upstream.yml@master + secrets: inherit diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5370a40..dbd9bd1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,8 +14,6 @@ jobs: name: Build test steps: - uses: actions/checkout@v4 - - name: Resolve the SDK commit to build - run: ./scripts/resolve-sdk-commit.sh - run: npx @dappnode/dappnodesdk build --skip_save release: @@ -25,11 +23,6 @@ jobs: if: github.event_name == 'push' || github.event_name == 'repository_dispatch' steps: - uses: actions/checkout@v4 - # Resolves dappnode-nexus-sdk main to the commit this build actually used - # and records it in the manifest, so the published package says which SDK - # source went into it without anyone maintaining the value by hand. - - name: Resolve the SDK commit to build - run: ./scripts/resolve-sdk-commit.sh - name: Publish run: npx @dappnode/dappnodesdk publish patch --dappnode_team_preset --timeout 2h env: diff --git a/Dockerfile b/Dockerfile index 2f5b53d..74350e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,17 +7,15 @@ FROM ${GO_IMAGE} AS build ARG UPSTREAM_VERSION RUN test -n "${UPSTREAM_VERSION}" \ - && test "$(printf '%s' "${UPSTREAM_VERSION}" | wc -c)" -eq 40 \ - && printf '%s' "${UPSTREAM_VERSION}" | grep -Eq '^[0-9a-f]{40}$' + && printf '%s' "${UPSTREAM_VERSION}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' RUN apk add --no-cache ca-certificates git WORKDIR /src RUN git init \ && git remote add origin https://github.com/dappnode/dappnode-nexus-sdk.git -RUN git fetch --depth=1 origin "${UPSTREAM_VERSION}" \ - && git checkout --detach FETCH_HEAD \ - && test "$(git rev-parse HEAD)" = "${UPSTREAM_VERSION}" +RUN git fetch --depth=1 origin "refs/tags/${UPSTREAM_VERSION}" \ + && git checkout --detach FETCH_HEAD RUN go mod download \ && go mod verify \ diff --git a/dappnode_package.json b/dappnode_package.json index 7aa3510..99f208e 100644 --- a/dappnode_package.json +++ b/dappnode_package.json @@ -1,7 +1,7 @@ { "name": "nexus-proxy.dnp.dappnode.eth", "version": "0.1.0", - "upstreamVersion": "39a2ccee325148b011c959de5118f355652ead52", + "upstreamVersion": "v0.1.0", "upstreamRepo": "dappnode/dappnode-nexus-sdk", "upstreamArg": "UPSTREAM_VERSION", "shortDescription": "Keep your Nexus prompts private, and verify it yourself", diff --git a/docker-compose.yml b/docker-compose.yml index edc1dee..46b9d30 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: context: . dockerfile: Dockerfile args: - UPSTREAM_VERSION: 39a2ccee325148b011c959de5118f355652ead52 + UPSTREAM_VERSION: v0.1.0 image: nexus-proxy.dnp.dappnode.eth:0.1.0 container_name: DAppNodeCore-nexus-proxy.dnp.dappnode.eth restart: unless-stopped diff --git a/scripts/resolve-sdk-commit.sh b/scripts/resolve-sdk-commit.sh deleted file mode 100755 index 7100664..0000000 --- a/scripts/resolve-sdk-commit.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh -# Resolve dappnode-nexus-sdk to the commit this build will use, and write it -# into the manifest and compose so the published package records its own -# provenance. -# -# This value is an output, not an input. Nothing verifies the proxy image -# against it -- the package is trusted because Dappnode published it onchain -- -# so hand-maintaining a pin bought a record that CI can produce for free, at -# the cost of a bump PR for every SDK change. -# -# The Dockerfile still requires a full 40-character commit SHA and re-checks it -# after fetching, so the build remains content-addressed. Only who supplies the -# value changed. -set -eu - -REPO="${SDK_REPO:-https://github.com/dappnode/dappnode-nexus-sdk.git}" -REF="${SDK_REF:-main}" - -COMMIT="$(git ls-remote "$REPO" "refs/heads/$REF" | cut -f1)" -if ! printf '%s' "$COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then - echo "could not resolve $REPO $REF to a commit (got: '$COMMIT')" >&2 - exit 1 -fi - -echo "SDK $REF resolves to $COMMIT" - -# Both files carry it: the manifest is what the published package shows, the -# compose build-arg is what the image is actually built from. -tmp="$(mktemp)" -sed -E "s/\"upstreamVersion\": \"[0-9a-f]{40}\"/\"upstreamVersion\": \"$COMMIT\"/" \ - dappnode_package.json > "$tmp" && mv "$tmp" dappnode_package.json -tmp="$(mktemp)" -sed -E "s/UPSTREAM_VERSION: [0-9a-f]{40}/UPSTREAM_VERSION: $COMMIT/" \ - docker-compose.yml > "$tmp" && mv "$tmp" docker-compose.yml - -grep -q "$COMMIT" dappnode_package.json && grep -q "$COMMIT" docker-compose.yml \ - || { echo "failed to stamp the resolved commit" >&2; exit 1; }