diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 142d6914c..6b998dd72 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260726.7", + "version": "5.260726.9", "source": "./plugins/genie", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project." } diff --git a/.github/workflows/musl-adapter-smoke.yml b/.github/workflows/musl-adapter-smoke.yml new file mode 100644 index 000000000..af2160876 --- /dev/null +++ b/.github/workflows/musl-adapter-smoke.yml @@ -0,0 +1,105 @@ +name: Musl Adapter Smoke + +# The Alpine execution adapter historically ran ONLY inside production release +# dogfood jobs, so every adapter defect cost a full release cycle to discover +# (docker pull noise on the stderr-strict probe, container-root vs host-uid git +# ownership). This smoke exercises the adapter on a real amd64 runner with the +# real previous-stable musl binary on every PR that touches it: seeding a git +# repository through the container exactly as the dogfood harness does, and +# asserting the probe's contract — exit 0 with a byte-empty stderr. +on: + pull_request: + paths: + - 'scripts/run-musl-dogfood.sh' + - '.github/workflows/musl-adapter-smoke.yml' + +permissions: + contents: read + +jobs: + smoke: + name: Seed a repository through the Alpine adapter + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + persist-credentials: false + + - name: Pre-pull the pinned Alpine image (mirrors the dogfood job) + run: docker pull -q 'alpine:3.19@sha256:6baf43584bcb78f2e5847d1de515f23499913ac9f12bdf834811a3145eb11ca1' + + - name: Download the previous-stable musl release binary + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + PREVIOUS_TAG="v$(jq -r '.version' .well-known/latest.json 2>/dev/null || true)" + if [[ "$PREVIOUS_TAG" == "v" || "$PREVIOUS_TAG" == "vnull" ]]; then + echo "::error::could not resolve the previous stable version from .well-known/latest.json" + exit 1 + fi + gh release download "$PREVIOUS_TAG" --repo "$GITHUB_REPOSITORY" \ + --pattern "genie-*-linux-x64-musl.tar.gz" --dir "$RUNNER_TEMP/previous" + + - name: Probe-mode contract — exit 0 with a byte-empty stderr + run: | + set -euo pipefail + ROOT="${RUNNER_TEMP}/dogfood-root" + mkdir -p "$ROOT/repos/a" "$ROOT/home" "$ROOT/genie-home/bin" "$ROOT/tmp" "$ROOT/n-extract" + tar -xzf "$RUNNER_TEMP"/previous/genie-*-linux-x64-musl.tar.gz -C "$ROOT/n-extract" + cp "$ROOT/n-extract/genie" "$ROOT/genie-home/bin/genie" + chmod 755 "$ROOT/genie-home/bin/genie" + cp "$ROOT/n-extract/VERSION" "$ROOT/genie-home/bin/VERSION" || true + # The harness's verifyCapability runs the adapter WITHOUT DOGFOOD_ROOT + # (candidate mounted read-only at /candidate) and rejects any stderr + # byte. --version is quiet on every generation, so bootstrap noise + # (docker pull progress, apk output) is the only thing that could + # reach stderr — exactly the class this contract pins. + status=0 + HOME="$ROOT/home" TMPDIR="$ROOT/tmp" TERM=xterm \ + bash scripts/run-musl-dogfood.sh "$ROOT/genie-home/bin/genie" --version \ + >"$RUNNER_TEMP/probe-out" 2>"$RUNNER_TEMP/probe-err" || status=$? + if [[ "$status" -ne 0 ]]; then + echo "::error::probe-mode adapter invocation exited ${status}" + cat "$RUNNER_TEMP/probe-out" "$RUNNER_TEMP/probe-err" + exit 1 + fi + if [[ -s "$RUNNER_TEMP/probe-err" ]]; then + echo "::error::probe-mode adapter invocation wrote stderr (the capability probe fails on any stderr byte):" + cat "$RUNNER_TEMP/probe-err" + exit 1 + fi + grep -q . "$RUNNER_TEMP/probe-out" || { echo "::error::--version produced no output"; exit 1; } + + - name: Stateful-mode seeding — the exact leg that failed in v5.260726.9 + run: | + set -euo pipefail + ROOT="${RUNNER_TEMP}/dogfood-root" + git init -q -b main "$ROOT/repos/a" + ADAPTER="$PWD/scripts/run-musl-dogfood.sh" + cd "$ROOT/repos/a" + run_via_adapter() { + local status=0 + DOGFOOD_ROOT="$ROOT" HOME="$ROOT/home" GENIE_HOME="$ROOT/genie-home" \ + TMPDIR="$ROOT/tmp" GENIE_RELEASE_DOGFOOD=1 TERM=xterm \ + bash "$ADAPTER" "$ROOT/genie-home/bin/genie" "$@" \ + >"$RUNNER_TEMP/cli-out" 2>"$RUNNER_TEMP/cli-err" || status=$? + if [[ "$status" -ne 0 ]]; then + echo "::error::adapter invocation 'genie $*' exited ${status}" + cat "$RUNNER_TEMP/cli-out" "$RUNNER_TEMP/cli-err" + exit 1 + fi + cat "$RUNNER_TEMP/cli-out" + } + run_via_adapter init + run_via_adapter task create --title smoke-sentinel --wish smoke + run_via_adapter board + # The database seeded through the container must be owned by the + # runner user, or every later host-side stage fails closed on + # genie's stat.uid ownership checks. + [[ "$(stat -c %u "$ROOT/repos/a/.genie/genie.db")" == "$(id -u)" ]] || { + echo "::error::container-created database is not owned by the host user" + exit 1 + } + echo "adapter smoke: OK" diff --git a/package.json b/package.json index 3e9771672..17606d884 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260726.7", + "version": "5.260726.9", "description": "Collaborative terminal toolkit for human + AI workflows. NOTE: npm distribution discontinued 2026-05-09 — install via `curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash` (cosign + SLSA verified). See https://automagik.dev/genie/release-process", "license": "MIT", diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json index eab8797b7..dcb6950f4 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260726.7", + "version": "5.260726.9", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.", "author": { "name": "Namastex Labs" diff --git a/plugins/genie/.codex-plugin/plugin.json b/plugins/genie/.codex-plugin/plugin.json index 2a178b170..f670a9bc5 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260726.7", + "version": "5.260726.9", "description": "Plan, execute, review, and ship software with Genie workflows in Codex.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index 46cc5aef6..43bf4a1a3 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260726.7", + "version": "5.260726.9", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", diff --git a/plugins/hermes-genie/plugin.yaml b/plugins/hermes-genie/plugin.yaml index 36941294e..4bbfea43a 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260726.7 +version: 5.260726.9 description: "Native Hermes surface for Genie orchestration: read-only status plus work-plan and review-plan gap tools that the MCP board surface does not cover, hooks, commands, and a thin cockpit skill. Board/task truth comes from the genie MCP tools." provides_tools: # Default surface: exactly the three gap tools the MCP board surface does not cover. diff --git a/scripts/run-musl-dogfood.sh b/scripts/run-musl-dogfood.sh index c06772c83..97bd8d137 100755 --- a/scripts/run-musl-dogfood.sh +++ b/scripts/run-musl-dogfood.sh @@ -52,6 +52,15 @@ fi # write pull progress to stderr, which the harness's stderr-strict capability # probe treats as failure. The dogfood workflow pre-pulls the digest. docker_args=(run --rm -i --security-opt no-new-privileges) + +# The candidate must run AS THE HOST USER, not container root. The fixture tree +# is bind-mounted and shared with host-side stages: git refuses a repository +# whose on-disk owner differs from the effective uid ("dubious ownership", so +# `genie init` reports "not a git repository"), genie's own fail-closed +# ownership checks demand stat.uid == process uid, and any root-owned file the +# container creates would poison every later host-side stage. Root is needed +# only for the apk bootstrap; su-exec then drops to this exact identity. +docker_args+=(--env "DOGFOOD_HOST_UID=$(id -u)" --env "DOGFOOD_HOST_GID=$(id -g)") container_candidate="/candidate/${candidate_name}" # The one-shot capability probe needs only a read-only candidate directory. @@ -98,8 +107,10 @@ exec docker "${docker_args[@]}" \ # Bootstrap noise must never reach stderr: the probe fails on any stderr # byte, and only the candidate binary output is meaningful to it. apk # failures still abort through set -e. - apk add --no-cache bash git libstdc++ >/dev/null 2>&1 + apk add --no-cache bash git libstdc++ su-exec >/dev/null 2>&1 candidate=$1 shift - exec "$candidate" "$@" + # Drop from bootstrap root to the exact host identity that owns the + # bind-mounted fixture tree (see DOGFOOD_HOST_UID rationale above). + exec su-exec "${DOGFOOD_HOST_UID}:${DOGFOOD_HOST_GID}" "$candidate" "$@" ' sh "$container_candidate" "$@" diff --git a/scripts/run-musl-dogfood.test.ts b/scripts/run-musl-dogfood.test.ts index cd17dbba4..8281d64b9 100644 --- a/scripts/run-musl-dogfood.test.ts +++ b/scripts/run-musl-dogfood.test.ts @@ -54,6 +54,13 @@ describe('musl dogfood execution adapter', () => { // the manifest every run and write pull progress to stderr, failing the // harness's stderr-strict capability probe. expect(args).not.toContain('--pull=always'); + // The candidate must execute as the host identity, not container root: + // the bind-mounted fixture tree is shared with host-side stages, and both + // git (dubious ownership) and genie's own ownership checks fail-close on a + // uid mismatch. Root exists only for the apk bootstrap; su-exec drops it. + expect(args).toContain(`DOGFOOD_HOST_UID=${process.getuid?.()}`); + expect(args).toContain(`DOGFOOD_HOST_GID=${process.getgid?.()}`); + expect(args.some((line) => line.includes('su-exec "${DOGFOOD_HOST_UID}:${DOGFOOD_HOST_GID}"'))).toBe(true); expect(args).toContain('alpine:3.19@sha256:6baf43584bcb78f2e5847d1de515f23499913ac9f12bdf834811a3145eb11ca1'); expect(args).toContain(`type=bind,src=${fx.root},dst=/candidate,readonly`); expect(args).toContain('$(touch should-not-exist); spaced');