diff --git a/.github/repository-settings.toml b/.github/repository-settings.toml index a4ffb12..7c61af0 100644 --- a/.github/repository-settings.toml +++ b/.github/repository-settings.toml @@ -13,7 +13,7 @@ [repository] default_branch = "master" -is_template = true +is_template = false immutable_releases = true has_wiki = false @@ -79,6 +79,7 @@ contexts = [ "ci", "Binary Release Dry Run", "Container Image Dry Run", + "Images Validate", ] [rulesets.tags_default] @@ -95,7 +96,7 @@ block_force_pushes = true bypass = [ { type = "repository_admin_role", mode = "always" }, # Release Please needs tag-creation bypass because tags are protected here. - { type = "app", slug = "meigma-release", mode = "always" }, + { type = "app", slug = "glab-release-please", mode = "always" }, ] [rulesets.tags_default.status_checks] diff --git a/.github/scripts/mcp_smoke.py b/.github/scripts/mcp_smoke.py index ea643f9..c423c85 100755 --- a/.github/scripts/mcp_smoke.py +++ b/.github/scripts/mcp_smoke.py @@ -13,7 +13,7 @@ The target is passed as a literal argv after `--`, so the same script covers every release artifact: - uv run .github/scripts/mcp_smoke.py -- ./bin/template-mcp-codemode stdio + uv run .github/scripts/mcp_smoke.py -- ./bin/agentcompute stdio uv run .github/scripts/mcp_smoke.py -- dist/release-assets/BINARY stdio uv run .github/scripts/mcp_smoke.py -- docker run -i --rm IMAGE stdio @@ -74,7 +74,7 @@ def parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]: ) parser.add_argument( "--server-name", - default="template-mcp-codemode", + default="agentcompute", help="exact initialize serverInfo.name the target must report", ) parser.add_argument( diff --git a/.github/scripts/stage_ghd_release_assets.py b/.github/scripts/stage_ghd_release_assets.py index a17e69d..38622ce 100644 --- a/.github/scripts/stage_ghd_release_assets.py +++ b/.github/scripts/stage_ghd_release_assets.py @@ -41,7 +41,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser.add_argument("--artifacts", default=Path("dist/artifacts.json"), type=Path) parser.add_argument("--config", default=Path("ghd.toml"), type=Path) parser.add_argument("--output", default=Path("dist/release-assets"), type=Path) - parser.add_argument("--binary-name", default="template-mcp-codemode") + parser.add_argument("--binary-name", default="agentcompute") return parser.parse_args(argv) diff --git a/.github/scripts/test_configure_github_repo.py b/.github/scripts/test_configure_github_repo.py index a9c9c62..2926430 100644 --- a/.github/scripts/test_configure_github_repo.py +++ b/.github/scripts/test_configure_github_repo.py @@ -102,8 +102,8 @@ def test_plan_creates_workflow_pages_site(self) -> None: plan = configure.build_plan( api, - "meigma", - "template-mcp-codemode", + "GilmanLab", + "agentcompute", base_config({"build_type": "workflow", "https_enforced": True}), mode="plan", hostname="github.com", @@ -119,8 +119,8 @@ def test_plan_updates_existing_pages_site(self) -> None: plan = configure.build_plan( api, - "meigma", - "template-mcp-codemode", + "GilmanLab", + "agentcompute", base_config({"build_type": "workflow", "https_enforced": True}), mode="plan", hostname="github.com", @@ -133,7 +133,7 @@ def test_plan_updates_existing_pages_site(self) -> None: def test_apply_create_pages_runs_follow_up_update(self) -> None: api = FakeGitHubApi() plan = configure.PlanResult( - repo="meigma/template-mcp-codemode", + repo="GilmanLab/agentcompute", hostname="github.com", mode="apply", changes=[ @@ -153,7 +153,7 @@ def test_apply_create_pages_runs_follow_up_update(self) -> None: warnings=[], ) - applied = configure.apply_plan(api, "meigma", "template-mcp-codemode", plan) + applied = configure.apply_plan(api, "GilmanLab", "agentcompute", plan) self.assertEqual(applied, ["Create GitHub Pages site"]) self.assertEqual(api.created_pages, [{"build_type": "workflow"}]) diff --git a/.github/scripts/test_stage_ghd_release_assets.py b/.github/scripts/test_stage_ghd_release_assets.py index 84bdc0e..e0d6082 100644 --- a/.github/scripts/test_stage_ghd_release_assets.py +++ b/.github/scripts/test_stage_ghd_release_assets.py @@ -61,36 +61,36 @@ def test_stages_expected_assets(self) -> None: staged, [ "checksums.txt", - "template-mcp-codemode_1.2.3_darwin_amd64", - "template-mcp-codemode_1.2.3_darwin_amd64.sbom.json", - "template-mcp-codemode_1.2.3_darwin_arm64", - "template-mcp-codemode_1.2.3_darwin_arm64.sbom.json", - "template-mcp-codemode_1.2.3_linux_amd64", - "template-mcp-codemode_1.2.3_linux_amd64.sbom.json", - "template-mcp-codemode_1.2.3_linux_arm64", - "template-mcp-codemode_1.2.3_linux_arm64.sbom.json", + "agentcompute_1.2.3_darwin_amd64", + "agentcompute_1.2.3_darwin_amd64.sbom.json", + "agentcompute_1.2.3_darwin_arm64", + "agentcompute_1.2.3_darwin_arm64.sbom.json", + "agentcompute_1.2.3_linux_amd64", + "agentcompute_1.2.3_linux_amd64.sbom.json", + "agentcompute_1.2.3_linux_arm64", + "agentcompute_1.2.3_linux_arm64.sbom.json", ], ) - linux_binary = root / "dist/release-assets/template-mcp-codemode_1.2.3_linux_amd64" + linux_binary = root / "dist/release-assets/agentcompute_1.2.3_linux_amd64" mode = linux_binary.stat().st_mode self.assertTrue(mode & stat.S_IXUSR) - self.assertIn("dist/release-assets/template-mcp-codemode_1.2.3_linux_arm64", stdout) + self.assertIn("dist/release-assets/agentcompute_1.2.3_linux_arm64", stdout) def test_fails_on_missing_checksum_entry(self) -> None: - with fixture(missing_checksum="template-mcp-codemode_1.2.3_linux_arm64") as root: + with fixture(missing_checksum="agentcompute_1.2.3_linux_arm64") as root: result, _, stderr = run_script(root) self.assertEqual(result, 1) self.assertIn("missing checksum entry", stderr) - self.assertIn("template-mcp-codemode_1.2.3_linux_arm64", stderr) + self.assertIn("agentcompute_1.2.3_linux_arm64", stderr) def test_fails_on_checksum_mismatch(self) -> None: - override = ("template-mcp-codemode_1.2.3_linux_amd64", "0" * 64) + override = ("agentcompute_1.2.3_linux_amd64", "0" * 64) with fixture(checksum_override=override) as root: result, _, stderr = run_script(root) self.assertEqual(result, 1) - self.assertIn("checksum mismatch for template-mcp-codemode_1.2.3_linux_amd64", stderr) + self.assertIn("checksum mismatch for agentcompute_1.2.3_linux_amd64", stderr) def test_fails_on_wrong_signer_workflow(self) -> None: with fixture(signer="other/repo/.github/workflows/attest.yml") as root: @@ -104,7 +104,7 @@ def test_fails_on_missing_os_arch_asset(self) -> None: result, _, stderr = run_script(root) self.assertEqual(result, 1) - self.assertIn("missing expected binary asset template-mcp-codemode_1.2.3_linux_arm64", stderr) + self.assertIn("missing expected binary asset agentcompute_1.2.3_linux_arm64", stderr) def test_fails_on_unexpected_asset_count(self) -> None: with fixture(extra_binary=True) as root: @@ -117,7 +117,7 @@ def test_fails_on_unexpected_asset_count(self) -> None: def run_script(root: Path) -> tuple[int, str, str]: stdout = io.StringIO() stderr = io.StringIO() - with working_directory(root), github_repository("meigma/template-mcp-codemode"): + with working_directory(root), github_repository("GilmanLab/agentcompute"): with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): result = stage_ghd_release_assets.main(["--tag", "v1.2.3"]) return result, stdout.getvalue(), stderr.getvalue() @@ -126,7 +126,7 @@ def run_script(root: Path) -> tuple[int, str, str]: @contextlib.contextmanager def fixture( *, - signer: str = "meigma/template-mcp-codemode/.github/workflows/attest.yml", + signer: str = "GilmanLab/agentcompute/.github/workflows/attest.yml", missing_checksum: str | None = None, checksum_override: tuple[str, str] | None = None, omit_artifact: tuple[str, str, str] | None = None, @@ -140,7 +140,7 @@ def fixture( artifacts: list[dict[str, str]] = [] checksum_entries: dict[str, str] = {} for goos, goarch in PLATFORMS: - binary_name = f"template-mcp-codemode_1.2.3_{goos}_{goarch}" + binary_name = f"agentcompute_1.2.3_{goos}_{goarch}" sbom_name = f"{binary_name}.sbom.json" binary_path = root / "dist" / binary_name @@ -163,7 +163,7 @@ def fixture( }) if extra_binary: - extra_name = "template-mcp-codemode_1.2.3_freebsd_amd64" + extra_name = "agentcompute_1.2.3_freebsd_amd64" extra_path = root / "dist" / extra_name extra_path.write_bytes(b"extra\n") artifacts.append({"type": "Binary", "name": extra_name, "path": f"dist/{extra_name}"}) @@ -197,32 +197,32 @@ def write_ghd_toml(path: Path, signer: str) -> None: signer_workflow = "{signer}" [[packages]] -name = "template-mcp-codemode" -description = "Meigma CodeMode MCP server template starter CLI." +name = "agentcompute" +description = "agentcompute CodeMode MCP server." tag_pattern = "v${{version}}" [[packages.assets]] os = "darwin" arch = "amd64" -pattern = "template-mcp-codemode_${{version}}_darwin_amd64" +pattern = "agentcompute_${{version}}_darwin_amd64" [[packages.assets]] os = "darwin" arch = "arm64" -pattern = "template-mcp-codemode_${{version}}_darwin_arm64" +pattern = "agentcompute_${{version}}_darwin_arm64" [[packages.assets]] os = "linux" arch = "amd64" -pattern = "template-mcp-codemode_${{version}}_linux_amd64" +pattern = "agentcompute_${{version}}_linux_amd64" [[packages.assets]] os = "linux" arch = "arm64" -pattern = "template-mcp-codemode_${{version}}_linux_arm64" +pattern = "agentcompute_${{version}}_linux_arm64" [[packages.binaries]] -path = "template-mcp-codemode" +path = "agentcompute" ''', encoding="utf-8", ) diff --git a/.github/workflows/images-publish.yml b/.github/workflows/images-publish.yml new file mode 100644 index 0000000..7802491 --- /dev/null +++ b/.github/workflows/images-publish.yml @@ -0,0 +1,512 @@ +# Builds the router image on a hosted runner, boot-tests the candidate on the +# lab Incus cluster, and publishes it to GHCR as an immutable imgoci release. +# +# Trust layout: +# * `build` holds no credentials and no environment. It is the same code path a +# pull request runs (images-validate.yml), only with the privileged build. +# * `publish` is the only job with credentials. It carries the `image-publish` +# environment (restricted to the default branch), reaches the lab over an +# ephemeral Tailscale node, boot-tests the candidate, then publishes. +# * `attest` runs in the ISOLATED reusable workflow so the provenance signing +# identity is unreachable from the build/publish steps (SLSA Build L3). +# +# Both jobs additionally require a protected default branch at runtime, so a +# pushed or dispatched run from anywhere else does nothing. +# +# Immutability: the published tag is derived from the git tree hash of images/, +# which is exactly the set of inputs that determines the image. Re-running with +# an unchanged images/ tree FAILS in `publish` ("release already exists") — the +# publisher refuses to replace a reference. Promote the existing digest instead +# of trying to overwrite it. +# +# Runner selection: both jobs run on `${{ vars.IMAGES_RUNNER || 'ubuntu-24.04' }}`. +# Unset — the default — means GitHub-hosted Ubuntu 24.04, which reaches the lab +# through an ephemeral Tailscale node. When a lab-side runner (scale set) exists, +# setting the repository variable IMAGES_RUNNER to its label moves both jobs +# there and automatically skips the tailnet enrollment step (`if: vars.IMAGES_RUNNER +# == ''`), because such a runner is already inside the network. That is a +# one-variable switch: no workflow edit, and every other guarantee here (pinned +# downloads, pinned TLS, boot test before publish, immutable tag, isolated +# attestation) is unchanged. Note that a self-hosted runner must stay off +# untrusted pull-request workloads — images-validate.yml never uses it. + +name: Router Image Publish + +on: + push: + branches: + - master + paths: + - images/** + - .github/workflows/images-publish.yml + workflow_dispatch: + +permissions: {} + +env: + IMAGE_REPOSITORY: ghcr.io/gilmanlab/agentcompute/router + +# Never cancel a run that may already be talking to the cluster or the registry. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + build: + name: Router Image Build + if: ${{ github.ref == 'refs/heads/master' && github.ref_protected }} + runs-on: ${{ vars.IMAGES_RUNNER || 'ubuntu-24.04' }} + permissions: + contents: read + outputs: + version: ${{ steps.identity.outputs.version }} + reference: ${{ steps.identity.outputs.reference }} + tree: ${{ steps.identity.outputs.tree }} + artifact: ${{ steps.identity.outputs.artifact }} + tar-sha256: ${{ steps.checksums.outputs.tar-sha256 }} + tar-bytes: ${{ steps.checksums.outputs.tar-bytes }} + # BUILD_ROOT is exported below rather than declared here: the `runner` + # context is not available in job-level `env`. + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Setup mise + uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0 + with: + version: 2026.6.14 + cache: true + + - name: Resolve image identity + id: identity + run: | + set -euo pipefail + + # The git tree hash of images/ covers the recipe, the pins and the + # build script: everything that decides what the image contains. + tree="$(git rev-parse 'HEAD:images')" + version="tree-${tree:0:12}" + + { + echo "tree=$tree" + echo "version=$version" + echo "reference=${IMAGE_REPOSITORY}:${version}" + echo "artifact=router-image-${version}" + } >> "$GITHUB_OUTPUT" + + echo "BUILD_ROOT=$RUNNER_TEMP/router" >> "$GITHUB_ENV" + + - name: Build the router image + run: | + set -euo pipefail + + # build.py creates --work-dir and --output-dir itself and refuses + # existing directories, so only the parent is created here. + mkdir -p "$BUILD_ROOT" + + # distrobuilder needs root to unpack and assemble the rootfs. `sudo + # env PATH=...` keeps the mise-pinned uv resolvable without exporting + # the runner user's HOME, so root uses its own caches. build.py + # downloads and SHA-verifies its own Go toolchain per images/pins.yaml. + sudo env PATH="$PATH" "$(mise which uv)" run --locked --script images/build.py \ + build --work-dir "$BUILD_ROOT/work" --output-dir "$BUILD_ROOT/out" + + sudo chown -R "$(id -u):$(id -g)" "$BUILD_ROOT/out" + + - name: Record artifact checksums + id: checksums + env: + VERSION: ${{ steps.identity.outputs.version }} + TREE: ${{ steps.identity.outputs.tree }} + run: | + set -euo pipefail + cd "$BUILD_ROOT/out" + + test -f router.tar.xz || { echo "build produced no router.tar.xz" >&2; exit 1; } + test -f metrics.json || { echo "build produced no metrics.json" >&2; exit 1; } + + sha256sum router.tar.xz metrics.json > checksums.txt + tar_sha256="$(awk '$2 == "router.tar.xz" { print $1 }' checksums.txt)" + + { + echo "tar-sha256=$tar_sha256" + echo "tar-bytes=$(stat -c %s router.tar.xz)" + } >> "$GITHUB_OUTPUT" + + { + echo "## Router image build" + echo + echo "- version: \`$VERSION\`" + echo "- images/ tree: \`$TREE\`" + echo "- router.tar.xz: \`$tar_sha256\` ($(stat -c %s router.tar.xz) bytes)" + echo + echo '```json' + cat metrics.json + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload router image artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ steps.identity.outputs.artifact }} + path: | + ${{ runner.temp }}/router/out/router.tar.xz + ${{ runner.temp }}/router/out/metrics.json + ${{ runner.temp }}/router/out/checksums.txt + if-no-files-found: error + retention-days: 7 + + publish: + name: Router Image Publish + if: ${{ github.ref == 'refs/heads/master' && github.ref_protected }} + needs: + - build + runs-on: ${{ vars.IMAGES_RUNNER || 'ubuntu-24.04' }} + # Credentials live ONLY here: the environment is restricted to the default + # branch and holds the Incus client/server certificates plus the private + # endpoint. No build step ever sees them. + environment: image-publish + permissions: + contents: read + packages: write # push the imgoci release to GHCR with GITHUB_TOKEN + id-token: write # Tailscale workload identity federation (no OAuth secret) + outputs: + digest: ${{ steps.publish.outputs.digest }} + reference: ${{ steps.publish.outputs.reference }} + subject-name: ${{ steps.publish.outputs.subject-name }} + # EVIDENCE and CANDIDATE are exported in the first run step instead: the + # `runner` context is not available in job-level `env`. + env: + GOTOOLCHAIN: local + INCUS_REMOTE: nas01 + INCUS_PROJECT: image-build + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Setup mise + uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0 + with: + version: 2026.6.14 + cache: true + + - name: Download the candidate image + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.build.outputs.artifact }} + path: ${{ runner.temp }}/artifact + + - name: Verify the candidate against the build checksums + env: + EXPECTED: ${{ needs.build.outputs.tar-sha256 }} + run: | + set -euo pipefail + + { + echo "EVIDENCE=$RUNNER_TEMP/evidence" + echo "CANDIDATE=$RUNNER_TEMP/artifact/router.tar.xz" + } >> "$GITHUB_ENV" + + EVIDENCE="$RUNNER_TEMP/evidence" + CANDIDATE="$RUNNER_TEMP/artifact/router.tar.xz" + mkdir -p "$EVIDENCE" + + cd "$(dirname "$CANDIDATE")" + sha256sum --check --strict checksums.txt + + actual="$(sha256sum router.tar.xz | cut -d' ' -f1)" + if [ "$actual" != "$EXPECTED" ]; then + echo "candidate sha256 $actual does not match the build job output $EXPECTED" >&2 + exit 1 + fi + cp checksums.txt metrics.json "$EVIDENCE/" + + - name: Install the pinned Incus client + run: | + set -euo pipefail + + # images/pins.yaml is the single source of truth for every downloaded + # input, including this client binary. Never install a floating + # distribution package: the client must match a reviewed pin. + pin() { + awk -v key="$1:" ' + /^[^[:space:]]/ { inside = ($0 == "incus:") } + inside && $1 == key { print $2; found = 1; exit } + END { if (!found) exit 1 } + ' images/pins.yaml + } + + version="$(pin version)" + url="$(pin url)" + sha256="$(pin sha256)" + + case "$url" in + https://github.com/lxc/incus/releases/download/*) ;; + *) echo "unexpected Incus download URL: $url" >&2; exit 1 ;; + esac + + bin="$RUNNER_TEMP/bin" + mkdir -p "$bin" + curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \ + --retry 3 --retry-connrefused --output "$bin/incus" "$url" + + printf '%s %s\n' "$sha256" "$bin/incus" | sha256sum --check --strict - + chmod 0755 "$bin/incus" + + reported="$("$bin/incus" --version)" + case "$reported" in + *"$version"*) ;; + *) echo "pinned Incus $version but the binary reports $reported" >&2; exit 1 ;; + esac + + echo "$bin" >> "$GITHUB_PATH" + echo "Installed SHA-verified Incus client $version" + + - name: Configure the Incus client + env: + INCUS_URL: ${{ vars.INCUS_URL }} + INCUS_CLIENT_CERT: ${{ secrets.INCUS_CLIENT_CERT }} + INCUS_CLIENT_KEY: ${{ secrets.INCUS_CLIENT_KEY }} + INCUS_SERVER_CERT: ${{ secrets.INCUS_SERVER_CERT }} + run: | + set -euo pipefail + + for name in INCUS_URL INCUS_CLIENT_CERT INCUS_CLIENT_KEY INCUS_SERVER_CERT; do + if [ -z "${!name:-}" ]; then + echo "$name is not configured for the image-publish environment; publication stays disabled" >&2 + exit 1 + fi + done + + case "$INCUS_URL" in + https://*) ;; + *) echo "INCUS_URL must be an https:// endpoint" >&2; exit 1 ;; + esac + + umask 077 + conf="$RUNNER_TEMP/incus" + mkdir -p "$conf/servercerts" + + printf '%s\n' "$INCUS_CLIENT_CERT" > "$conf/client.crt" + printf '%s\n' "$INCUS_CLIENT_KEY" > "$conf/client.key" + # The server certificate is pinned here, so the client trusts exactly + # this cluster and nothing the public CA set would also accept. + printf '%s\n' "$INCUS_SERVER_CERT" > "$conf/servercerts/${INCUS_REMOTE}.crt" + + # Shape checks only — contents are never printed. + grep -q 'BEGIN CERTIFICATE' "$conf/client.crt" || { echo "INCUS_CLIENT_CERT is not a PEM certificate" >&2; exit 1; } + grep -q 'BEGIN CERTIFICATE' "$conf/servercerts/${INCUS_REMOTE}.crt" || { echo "INCUS_SERVER_CERT is not a PEM certificate" >&2; exit 1; } + grep -q 'PRIVATE KEY' "$conf/client.key" || { echo "INCUS_CLIENT_KEY is not a PEM private key" >&2; exit 1; } + + { + printf 'default-remote: %s\n' "$INCUS_REMOTE" + printf 'remotes:\n' + printf ' %s:\n' "$INCUS_REMOTE" + printf ' addr: %s\n' "$INCUS_URL" + printf ' auth_type: tls\n' + printf ' project: %s\n' "$INCUS_PROJECT" + printf ' protocol: incus\n' + printf ' public: false\n' + } > "$conf/config.yml" + + echo "INCUS_CONF=$conf" >> "$GITHUB_ENV" + + # Only hosted runners need to join the tailnet. A lab runner (IMAGES_RUNNER + # set) is already inside the network, so enrollment is skipped there. + - name: Connect to the tailnet + if: ${{ vars.IMAGES_RUNNER == '' }} + uses: tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3 + with: + # Workload identity federation: a GitHub OIDC token is exchanged for an + # ephemeral node, so no long-lived Tailscale secret exists anywhere. + oauth-client-id: ${{ vars.TS_CLIENT_ID }} + audience: ${{ vars.TS_AUDIENCE }} + tags: tag:sandbox + # The cluster API sits behind the gateway's advertised lab subnet route. + args: --accept-routes + version: 1.94.2 + sha256sum: c6f99a5d774c7783b56902188d69e9756fc3dddfb08ac6be4cb2585f3fecdc32 + # Do not populate a privileged job from the Actions cache. + use-cache: false + + - name: Verify the Incus endpoint over the tailnet + run: | + set -euo pipefail + + # Tailnet peer propagation is eventually consistent, so retry the very + # first API call instead of failing on a cold tailnet. + reachable=0 + for attempt in $(seq 1 12); do + if incus query "${INCUS_REMOTE}:/1.0?project=${INCUS_PROJECT}" > "$RUNNER_TEMP/incus-server.json"; then + reachable=1 + break + fi + echo "Incus API not reachable yet (attempt $attempt); retrying in 5s" + sleep 5 + done + + if [ "$reachable" -ne 1 ]; then + echo "the configured Incus endpoint is unreachable over the tailnet; publication stays disabled" >&2 + exit 1 + fi + + # /1.0 answers untrusted clients too, so prove the pinned client + # certificate is actually trusted for this project by reading a + # protected collection (403 for an untrusted or misscoped cert). + if ! incus query "${INCUS_REMOTE}:/1.0/images?project=${INCUS_PROJECT}&recursion=1" > "$RUNNER_TEMP/incus-images.json"; then + echo "the client certificate is not trusted for project ${INCUS_PROJECT}; publication stays disabled" >&2 + exit 1 + fi + + echo "Incus endpoint reachable and the client certificate is trusted for ${INCUS_PROJECT}" + + - name: Boot smoke test the candidate + run: | + set -euo pipefail + + # Boot the LOCAL artifact before anything is published. The script + # never promotes the shared `router` alias and removes only the + # instance/alias/image it created; a cleanup failure fails this job. + spikes/images/smoke.sh \ + --file "$CANDIDATE" \ + --remote "$INCUS_REMOTE" \ + --project "$INCUS_PROJECT" \ + --suffix "ci-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \ + --log "$EVIDENCE/smoke.log" \ + > "$EVIDENCE/smoke.json" + + cat "$EVIDENCE/smoke.json" + + - name: Build the router publisher + working-directory: spikes/images + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/bin" + go build -o "$RUNNER_TEMP/bin/images" . + + - name: Publish the immutable router release + id: publish + env: + GHCR_USERNAME: ${{ github.actor }} + GHCR_TOKEN: ${{ github.token }} + REFERENCE: ${{ needs.build.outputs.reference }} + VERSION: ${{ needs.build.outputs.version }} + run: | + set -euo pipefail + + # The publisher refuses to replace an existing reference, so a repeat + # run on an unchanged images/ tree fails here by design. + "$RUNNER_TEMP/bin/images" publish \ + --ref "$REFERENCE" \ + --version "$VERSION" \ + --file "$CANDIDATE" \ + > "$EVIDENCE/publish.json" + + cat "$EVIDENCE/publish.json" + + digest="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["digest"])' "$EVIDENCE/publish.json")" + case "$digest" in + sha256:*) ;; + *) echo "publisher returned an unexpected digest: $digest" >&2; exit 1 ;; + esac + + { + echo "digest=$digest" + echo "reference=$REFERENCE" + echo "subject-name=$IMAGE_REPOSITORY" + } >> "$GITHUB_OUTPUT" + + - name: Verify the published release by digest + env: + GHCR_USERNAME: ${{ github.actor }} + GHCR_TOKEN: ${{ github.token }} + DIGEST: ${{ steps.publish.outputs.digest }} + EXPECTED: ${{ needs.build.outputs.tar-sha256 }} + run: | + set -euo pipefail + + "$RUNNER_TEMP/bin/images" fetch \ + --ref "${IMAGE_REPOSITORY}@${DIGEST}" \ + --output "$RUNNER_TEMP/fetched.tar.xz" \ + > "$EVIDENCE/fetch.json" + + cat "$EVIDENCE/fetch.json" + + published="$(sha256sum "$RUNNER_TEMP/fetched.tar.xz" | cut -d' ' -f1)" + if [ "$published" != "$EXPECTED" ]; then + echo "published content $published does not match the built artifact $EXPECTED" >&2 + exit 1 + fi + echo "Published release round-trips to the built bytes ($published)" + + - name: Upload publication evidence + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: router-publish-${{ needs.build.outputs.version }} + path: ${{ runner.temp }}/evidence + # Evidence is uploaded even on failure, where earlier steps may not + # have produced any file yet; a missing directory must not mask the + # real failure. + if-no-files-found: warn + retention-days: 30 + + - name: Write publication summary + env: + DIGEST: ${{ steps.publish.outputs.digest }} + REFERENCE: ${{ needs.build.outputs.reference }} + VERSION: ${{ needs.build.outputs.version }} + TREE: ${{ needs.build.outputs.tree }} + TAR_SHA256: ${{ needs.build.outputs.tar-sha256 }} + TAR_BYTES: ${{ needs.build.outputs.tar-bytes }} + run: | + { + echo "## Router image published" + echo + echo "- reference: \`$REFERENCE\`" + echo "- release index digest: \`$DIGEST\`" + echo "- images/ tree: \`$TREE\`" + echo "- router.tar.xz: \`$TAR_SHA256\` ($TAR_BYTES bytes)" + echo + echo "The tag is immutable: republishing \`$VERSION\` is refused, so a rebuild of the" + echo "same images/ tree cannot overwrite it. Change images/ or promote this digest." + echo + echo "Promote to the fleet (imports, boot-tests, then atomically moves the \`router\` alias):" + echo + echo '```sh' + echo "go run ./spikes/images import --ref \"${IMAGE_REPOSITORY}@${DIGEST}\" --remote ${INCUS_REMOTE} --project ${INCUS_PROJECT}" + echo '```' + echo + echo "Verify provenance (signed in the isolated attest.yml workflow):" + echo + echo '```sh' + echo "gh attestation verify \"oci://${IMAGE_REPOSITORY}@${DIGEST}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/attest.yml\" --deny-self-hosted-runners" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + # Provenance for the imgoci release INDEX (not a layer), generated in the + # isolated reusable workflow so the signing identity is unreachable from the + # build and publish steps. + attest: + needs: + - publish + permissions: + id-token: write + attestations: write + contents: read + # attest.yml's job declares packages: write for its optional registry + # push; a reusable workflow cannot exceed the caller's grant, so the + # caller must offer it even though push-to-registry is false here. + packages: write + uses: ./.github/workflows/attest.yml + with: + subject-name: ${{ needs.publish.outputs.subject-name }} + subject-digest: ${{ needs.publish.outputs.digest }} + # Provenance stays in GitHub's attestation API. An imgoci release index is + # not a runnable container image, so nothing consumes a registry referrer; + # `gh attestation verify oci://...` works either way. + push-to-registry: false diff --git a/.github/workflows/images-validate.yml b/.github/workflows/images-validate.yml new file mode 100644 index 0000000..520b65b --- /dev/null +++ b/.github/workflows/images-validate.yml @@ -0,0 +1,87 @@ +# Credential-free pull-request gate for the router image build inputs. +# +# This workflow executes contributor-visible code (images/build.py, the +# distrobuilder recipe, the publisher module) and therefore holds NO secrets, no +# environment, and read-only permissions. Everything that needs credentials or +# network access to the lab lives in images-publish.yml, which only runs on the +# protected default branch. +# +# `build.py validate` is the contract for "the build inputs are coherent": it +# checks the recipe against images/pins.yaml without downloading anything, and +# `uv run --locked` fails closed when images/build.py.lock is out of date with +# the script's inline (PEP 723) dependency metadata. + +name: Images Validate + +on: + # Deliberately NOT path-filtered for pull requests: a path-filtered check can + # never be a required status check (it stays pending forever on pull requests + # that do not touch the filtered paths). The job is cheap — validation + # downloads nothing — so it runs on every pull request and can gate merges. + pull_request: + branches: + - master + push: + branches: + - master + paths: + - images/** + - spikes/images/** + - .github/workflows/images-validate.yml + - .github/workflows/images-publish.yml + workflow_dispatch: + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Images Validate + runs-on: ubuntu-24.04 + permissions: + contents: read + env: + # Never auto-download a Go toolchain other than the mise-pinned one + # (mise [env] is not carried by the action's shims, so set it on the job). + GOTOOLCHAIN: local + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Setup mise + uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0 + with: + version: 2026.6.14 + cache: true + # Installs every tool from mise.toml honoring mise.lock (locked → fail + # closed), including the uv and Go versions used below. + + - name: Resolve uv cache directory + id: uv-cache + run: echo "dir=$(uv cache dir)" >> "$GITHUB_OUTPUT" + + - name: Cache uv downloads + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ steps.uv-cache.outputs.dir }} + key: ${{ runner.os }}-uv-images-${{ hashFiles('images/build.py.lock', 'images/build.py') }} + restore-keys: | + ${{ runner.os }}-uv-images- + + - name: Validate recipe against pins + run: uv run --locked --script images/build.py validate + + # The publisher is its own Go module, so the repository's moon CI gate does + # not compile it. Build it here, credential-free, so a broken publisher is + # caught in review instead of on publish day. + - name: Build router publisher + working-directory: spikes/images + run: go build -o "$RUNNER_TEMP/images" . + + - name: Check the boot smoke script + run: bash -n spikes/images/smoke.sh diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml index 8bee263..04c38f5 100644 --- a/.github/workflows/release-dry-run.yml +++ b/.github/workflows/release-dry-run.yml @@ -73,7 +73,7 @@ jobs: set -euo pipefail version="0.0.0-dryrun.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}" - binary_name="template-mcp-codemode" + binary_name="agentcompute" host_os="$(go env GOHOSTOS)" host_arch="$(go env GOHOSTARCH)" bin="dist/release-assets/${binary_name}_${version}_${host_os}_${host_arch}" @@ -211,19 +211,19 @@ jobs: run: | set -euo pipefail - apko build apko.yaml template-mcp-codemode:dry-run image.tar \ + apko build apko.yaml agentcompute:dry-run image.tar \ --arch amd64 \ --keyring-append ./melange-amd64.rsa.pub \ --keyring-append ./melange-arm64.rsa.pub docker load < image.tar - docker tag template-mcp-codemode:dry-run-amd64 template-mcp-codemode:dry-run + docker tag agentcompute:dry-run-amd64 agentcompute:dry-run - docker run --rm template-mcp-codemode:dry-run --version - docker run --rm template-mcp-codemode:dry-run --help + docker run --rm agentcompute:dry-run --version + docker run --rm agentcompute:dry-run --help # Real MCP session against the assembled image. `docker run -i` keeps stdin # open for the stdio transport; the run also proves the CodeMode worker can # re-execute itself as the image's nonroot user with no shell present. uv run .github/scripts/mcp_smoke.py \ - -- docker run -i --rm template-mcp-codemode:dry-run stdio + -- docker run -i --rm agentcompute:dry-run stdio diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index e3807ea..1adfefe 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -1,6 +1,6 @@ # Required release app settings: -# - vars.MEIGMA_RELEASE_APP_CLIENT_ID -# - secrets.MEIGMA_RELEASE_APP_PRIVATE_KEY +# - vars.RELEASE_APP_CLIENT_ID +# - secrets.RELEASE_APP_PRIVATE_KEY # - protected-tag ruleset bypass for the release app, because this workflow # creates protected v* tags after the release PR is merged. @@ -31,8 +31,8 @@ jobs: id: release-app uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - client-id: ${{ vars.MEIGMA_RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.MEIGMA_RELEASE_APP_PRIVATE_KEY }} + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - name: Run Release Please uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f72bedf..2c4f7b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ on: permissions: {} env: - IMAGE_NAME: ghcr.io/meigma/template-mcp-codemode + IMAGE_NAME: ghcr.io/gilmanlab/agentcompute jobs: resolve-release: @@ -126,7 +126,7 @@ jobs: set -euo pipefail version="${RELEASE_TAG#v}" - binary_name="template-mcp-codemode" + binary_name="agentcompute" host_os="$(go env GOHOSTOS)" host_arch="$(go env GOHOSTARCH)" bin="dist/release-assets/${binary_name}_${version}_${host_os}_${host_arch}" @@ -444,9 +444,9 @@ jobs: echo echo '```sh' echo "gh release view $RELEASE_TAG --repo $GITHUB_REPOSITORY --json isDraft,assets" - echo "asset=\"template-mcp-codemode_${RELEASE_VERSION}_\$(go env GOOS)_\$(go env GOARCH)\"" + echo "asset=\"agentcompute_${RELEASE_VERSION}_\$(go env GOOS)_\$(go env GOARCH)\"" echo "gh attestation verify \"dist/release-assets/\${asset}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/attest.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners" - echo "ghd download \"$GITHUB_REPOSITORY/template-mcp-codemode@${RELEASE_VERSION}\" --output \"\$(mktemp -d)\"" + echo "ghd download \"$GITHUB_REPOSITORY/agentcompute@${RELEASE_VERSION}\" --output \"\$(mktemp -d)\"" echo "uv run .github/scripts/mcp_smoke.py -- \"dist/release-assets/\${asset}\" stdio" echo '```' echo diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index a46a85d..be911d3 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -50,16 +50,16 @@ jobs: melange keygen melange.rsa melange build melange.yaml --arch amd64 --runner docker \ --signing-key melange.rsa --source-dir . --vars-file melange-vars.yaml - apko build apko.yaml template-mcp-codemode:security-scan scan.tar \ + apko build apko.yaml agentcompute:security-scan scan.tar \ --arch amd64 --keyring-append ./melange.rsa.pub docker load < scan.tar - docker tag template-mcp-codemode:security-scan-amd64 template-mcp-codemode:security-scan + docker tag agentcompute:security-scan-amd64 agentcompute:security-scan - name: Scan local image uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: scan-type: image - image-ref: template-mcp-codemode:security-scan + image-ref: agentcompute:security-scan scanners: vuln,secret,config vuln-type: os,library severity: HIGH,CRITICAL diff --git a/.golangci.yml b/.golangci.yml index 16dd55a..f95f6a3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -36,7 +36,7 @@ formatters: # with the given prefixes are grouped after 3rd-party packages. # Default: [] local-prefixes: - - github.com/meigma/template-mcp-codemode + - github.com/GilmanLab/agentcompute golines: # Target maximum line length. diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5a3dc8a..6393caf 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,15 +1,15 @@ version: 2 -project_name: template-mcp-codemode +project_name: agentcompute before: hooks: - go test ./... builds: - - id: template-mcp-codemode - main: ./cmd/template-mcp-codemode - binary: template-mcp-codemode + - id: agentcompute + main: ./cmd/agentcompute + binary: agentcompute env: - CGO_ENABLED=0 goos: @@ -28,9 +28,9 @@ builds: mod_timestamp: '{{ .CommitTimestamp }}' archives: - - id: template-mcp-codemode + - id: agentcompute ids: - - template-mcp-codemode + - agentcompute formats: - binary name_template: >- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7279232..b4d4f32 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -This repository is a Go CodeMode MCP server template. Keep changes focused, preserve the generated-project path, and route private vulnerability reports through [SECURITY.md](SECURITY.md). +This repository is a Go CodeMode MCP server. Keep changes focused and route private vulnerability reports through [SECURITY.md](SECURITY.md). ## Report a bug @@ -41,7 +41,7 @@ moon run root:lint moon run root:build moon run root:test moon run docs:serve -go run ./cmd/template-mcp-codemode --version +go run ./cmd/agentcompute --version ``` The STDIO server blocks until its client closes input or the process receives a signal. This is expected. macOS does not include `timeout` or `gtimeout` by default; use another time-bounding mechanism or install coreutils when a local script needs one. diff --git a/DELETE_ME.md b/DELETE_ME.md deleted file mode 100644 index 39a5416..0000000 --- a/DELETE_ME.md +++ /dev/null @@ -1,206 +0,0 @@ -# Set up a repository created from the CodeMode template - -This repository was generated from `template-mcp-codemode`. It includes a transport-agnostic CodeMode runtime, STDIO and Streamable HTTP transports, one demo capability, a development proxy, documentation, CI, and release configuration. - -Complete this checklist before feature work, then delete this file. - -## Template layout - -- `cmd/template-mcp-codemode` is the thin executable entry point. `codemode.ServeWorkerAndExit()` is its first statement. -- `internal/cli` constructs the Cobra command tree, resolves configuration, selects trusted subjects, and runs each transport. -- `internal/mcpserver` builds one immutable CodeMode runtime, registers capabilities, and adapts it to the three MCP tools `search_api`, `describe_api`, and `execute`. -- `internal/templateinfo` owns the binary name, client-visible title, and derived environment-variable prefix. -- `tools/proxy` is a nested Go module that rebuilds and swaps the STDIO child during development. - -The HTTP command constructs one runtime and MCP server at startup and shares them across all MCP sessions. Keep database pools, clients, and other shared dependencies in `mcpserver.Options.Deps`; do not construct a runtime per session. - -## Collect the new identity - -Choose each value independently: - -| Variable | Template value | Used for | -| --- | --- | --- | -| `OWNER` | `meigma` | Go modules, repository URLs, GHCR image, docs, and signer workflow. | -| `REPO` | `template-mcp-codemode` | Repository name, root module suffix, GHCR image, and docs URLs. | -| `BINARY` | `template-mcp-codemode` | `cmd` directory, executable, build output, release assets, and container entry point. | -| `NAME` | `template-mcp-codemode` | `templateinfo.Name`, Cobra command, MCP implementation name, and environment prefix. | -| `TITLE` | `Meigma CodeMode MCP server template` | `templateinfo.Title` and the client-visible MCP implementation title. | - -Derived values: - -- Root module: `github.com/OWNER/REPO` -- Proxy module: `github.com/OWNER/REPO/tools/proxy` -- Environment prefix: uppercase `NAME` with hyphens replaced by underscores (`template-mcp-codemode` becomes `TEMPLATE_MCP_CODEMODE`) -- Image: `ghcr.io/OWNER/REPO` - -Do not collapse these values into one global replacement. A repository name, binary name, client-visible title, and environment prefix can differ. - -## Files to regenerate or reset - -Do not blindly rewrite generated or historical files during the identity search: - -- Reset `CHANGELOG.md` to one `# Changelog` heading. Release Please writes the new project's history. -- Regenerate `docs/uv.lock` with `uv lock` after changing `docs/pyproject.toml`. -- Let `go mod tidy` update each `go.sum`. -- Ignore generated output such as `bin/`, `coverage.out`, `dist/`, and `docs/build/`. -- Do not rename this file. Delete it after the checklist is complete. - -## Rename the project - -### 1. Rename both Go modules - -The root and development proxy are separate modules: - -```sh -go mod edit -module github.com/OWNER/REPO -(cd tools/proxy && go mod edit -module github.com/OWNER/REPO/tools/proxy) -``` - -Update imports that refer to the template module. Preserve the `github.com/meigma/codemode` dependency and imports; CodeMode is the runtime library, not a template identity surface. - -### 2. Rename the binary - -```sh -mv cmd/template-mcp-codemode cmd/BINARY -``` - -Update every build-source and output path, including: - -- root `moon.yml` -- `.goreleaser.yaml` -- `melange.yaml` -- `apko.yaml` -- `ghd.toml` -- release and security-scan workflows -- `tools/proxy/internal/cli/defaults.go` -- `.mcp.json` if its invocation changes -- README and documentation commands - -### 3. Rename application identity - -Update `Name` and `Title` in `internal/templateinfo/info.go`. `Name` controls the Cobra command, MCP implementation name, and environment prefix. `Title` is reported to MCP clients. - -Search for every template identity, including human-readable variants: - -```sh -rg -i "template-mcp-codemode|TEMPLATE_MCP_CODEMODE|Meigma CodeMode MCP server template|meigma" -``` - -Map each result to `OWNER`, `REPO`, `BINARY`, `NAME`, or `TITLE`. Update the root and proxy module paths, repository URLs, package names, binary paths, container image, release assets, `ghd.toml` signer workflow, documentation metadata, and environment-variable examples. - -Do not replace upstream CodeMode names or links. The fixed adapter default implementation identity `codemode` and its three MCP tool names also belong to the upstream protocol surface, not this repository's brand. - -## Preserve worker wiring - -CodeMode re-executes the final binary for each program worker. This line must remain the first statement of `main`: - -```go -func main() { - codemode.ServeWorkerAndExit() - // ordinary host setup follows -} -``` - -It must precede signal setup, flag parsing, credentials, clients, authorizers, handlers, and transports. Package initialization still runs before `main`, so do not put privileged setup or irreversible side effects in package initializers. - -Every test package that calls `Builder.Build` needs: - -```go -func TestMain(m *testing.M) { - codemode.ServeWorkerAndExit() - os.Exit(m.Run()) -} -``` - -Keep the worker call as the first statement. A test package that never builds a CodeMode server does not need `TestMain`. - -## Replace the demo capability - -Add your real capabilities before removing `random.int` so the server remains useful throughout the cutover. For each capability: - -1. Define non-pointer input and output structs with supported fields and JSON tags. -2. Use `int64` for integer inputs; CodeMode does not accept platform-sized `int` input fields. -3. Register a stable capability ID, dotted name, discovery metadata, and typed handler through `codemode.Register`. -4. Pass shared collaborators through `mcpserver.Dependencies` and close over them in the handler. -5. Add behavior-focused tests and update the expected capability catalog. -6. Delete `randomint.go`, its tests, and its registration after the replacement capabilities are registered. - -Do not register each capability as a direct MCP tool. The MCP surface remains exactly `search_api`, `describe_api`, and `execute`. - -Keep `codemode.ServeWorkerAndExit` in the final binary and applicable test binaries. Keep the CodeMode module dependency and the `mcpserver.Options.Runtime` construction even after the demo capability is removed. - -## Replace demo identity and authorization - -The template's identity and policy wiring is explicit: - -- STDIO uses `mcpserver.StaticSubject` because local process ownership is its authentication boundary. -- HTTP uses `mcpserver.ContextSubject`. The MCP receiving middleware copies the SDK-authenticated `req.GetExtra().TokenInfo.UserID` into `authz.WithSubject`; an arbitrary value added only to the outer `net/http` request context is not the adapter's identity channel. -- The demo verifier sets `TokenInfo.UserID` to `shared-token`. Loopback or explicit `--insecure` requests without authentication use `development`. These are development identities, not production principals. -- The CLI passes `authz.AllowAll()` so the demo permits every call. `internal/mcpserver.New` has no hidden authorization fallback. - -For production HTTP, replace the shared-token verifier with real authentication that sets a stable, non-secret `auth.TokenInfo.UserID`. Keep the receiving-middleware bridge and `mcpserver.ContextSubject`; the bridge installs that ID as an `authz.Subject` with `authz.WithSubject` on the MCP handler context. Replace `AllowAll` with an authorizer appropriate for the enabled capabilities and their canonical arguments. Do not derive identity from Starlark source, MCP tool arguments, `_meta`, unvalidated headers, or arbitrary outer HTTP context values. - -Discovery is not authorization-filtered. Every authenticated subject can search and describe every statically enabled capability. Do not place secrets or tenant-sensitive details in discovery metadata; use static capability disabling when a deployment must hide a capability's existence. - -## Choose a transport - -The template includes both transports: - -- Keep STDIO for a server launched as a local subprocess. -- Keep Streamable HTTP for a remote or containerized server. - -To remove a transport, delete its file in `internal/cli` and its registration in `internal/cli/root.go`. Capability registration remains in `internal/mcpserver`. - -If you keep HTTP, preserve the one-runtime-at-startup design. Do not move `mcpserver.New` into the SDK's per-session factory. - -## Configure releases - -The template starts at Release Please baseline `0.0.0`; its first pending release is `0.1.0`. A generated project must keep its own changelog and release history. - -For a binary plus container release: - -- Update `.goreleaser.yaml`: project, build ID, main package, binary, archive names, and package paths. -- Update `ghd.toml`: signer workflow, package name, description, asset patterns, and installed path. -- Update `melange.yaml`: package name, description, Go package, and output. -- Update `apko.yaml`: local package, entry point, command, image annotations, and source URL. -- Update the release, dry-run, and security-scan workflows: image name, binary validation paths, smoke commands, and summaries. -- Update `release-please-config.json` and keep `.release-please-manifest.json` at the intended initial baseline. -- Configure the release GitHub App credentials, protected-tag bypass, and package permissions. Release Please expects `vars.MEIGMA_RELEASE_APP_CLIENT_ID` and `secrets.MEIGMA_RELEASE_APP_PRIVATE_KEY`. Install the app on the generated repository and explicitly grant that repository access to these values; template generation does not copy credential access or app installations. - -If the project is binary-only, remove the melange/apko jobs, image scan, image configuration, and container required checks. If it is container-only, remove GoReleaser, `ghd.toml`, binary jobs, and binary required checks. Keep the release dry run for every release path that remains. - -## Update documentation - -Rewrite `README.md` and `docs/docs/` for the real capabilities and retained transports. Update `docs/mkdocs.yml` (`site_url`, `repo_name`, `repo_url`, and `edit_uri`) for the generated repository. Review `CONTRIBUTING.md` and `SECURITY.md` and update the license holder if needed. - -If you retain the Pages workflow, enable GitHub Pages for the generated repository and select **GitHub Actions** as its build source. Template generation does not copy this repository setting. - -Link to the [canonical CodeMode documentation](https://meigma.github.io/codemode/) for the complete runtime, type, MCP tool, and security contracts rather than copying the upstream reference into the generated project. - -## Regenerate and verify - -Regenerate module and documentation metadata: - -```sh -go mod tidy -(cd tools/proxy && go mod tidy) -(cd docs && uv lock) -``` - -Run the repository gate: - -```sh -moon run root:check -``` - -Then repeat the identity search. It should return no template-owned identity except intentional historical context that you reviewed: - -```sh -rg -i "template-mcp-codemode|TEMPLATE_MCP_CODEMODE|Meigma CodeMode MCP server template|meigma" -``` - -Finally, build the renamed binary and use a real MCP client to call `search_api`, `describe_api`, and `execute` against one replacement capability over the retained transport. Delete this file after those checks pass: - -```sh -rm DELETE_ME.md -``` diff --git a/LICENSE-MIT b/LICENSE-MIT index 6dbc1f6..9682d33 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Meigma +Copyright (c) 2026 GilmanLab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6adf8f0..ffc6f55 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# template-mcp-codemode +# agentcompute -`template-mcp-codemode` is a Go template for building [Model Context Protocol](https://modelcontextprotocol.io) servers with [CodeMode](https://github.com/meigma/codemode). Instead of registering one MCP tool per operation, you register typed Go capabilities. An agent discovers them and composes several calls in one bounded Starlark program. +`agentcompute` is a [CodeMode](https://github.com/meigma/codemode) [Model Context Protocol](https://modelcontextprotocol.io) server. Instead of registering one MCP tool per operation, it registers typed Go capabilities. An agent discovers them and composes several calls in one bounded Starlark program. -Every server created from this template exposes exactly three MCP tools: +The server exposes exactly three MCP tools: - `search_api` finds capabilities by name, summary, and search terms. - `describe_api` returns the exact input and output shape for one capability. @@ -30,13 +30,13 @@ mise install Run the local STDIO transport: ```sh -go run ./cmd/template-mcp-codemode stdio +go run ./cmd/agentcompute stdio ``` Run Streamable HTTP on its loopback default: ```sh -go run ./cmd/template-mcp-codemode http --addr localhost:8080 +go run ./cmd/agentcompute http --addr localhost:8080 ``` Both commands build one immutable CodeMode runtime through `internal/mcpserver`. The HTTP command constructs the runtime and MCP server once at startup and shares them across sessions; it does not rebuild the capability catalog per request or per session. @@ -44,14 +44,14 @@ Both commands build one immutable CodeMode runtime through `internal/mcpserver`. For a local MCP client, build the binary and configure its absolute path: ```sh -go build -o bin/template-mcp-codemode ./cmd/template-mcp-codemode +go build -o bin/agentcompute ./cmd/agentcompute ``` ```json { "mcpServers": { - "template-mcp-codemode": { - "command": "/absolute/path/to/template-mcp-codemode/bin/template-mcp-codemode", + "agentcompute": { + "command": "/absolute/path/to/agentcompute/bin/agentcompute", "args": ["stdio"] } } @@ -110,7 +110,7 @@ Add one applicable `TestMain` per Go package. Do not put setup before the worker ## Identity and authorization -The template keeps authentication identity outside program source, tool arguments, and MCP metadata: +The server keeps authentication identity outside program source, tool arguments, and MCP metadata: - STDIO uses `mcpserver.StaticSubject` with the non-secret subject ID `local`. Process ownership is the authentication boundary. - HTTP uses `mcpserver.ContextSubject`. The receiving MCP middleware reads the SDK-authenticated `req.GetExtra().TokenInfo.UserID`, stores that non-secret identity with `authz.WithSubject`, and then lets the CodeMode adapter resolve it. Setting an arbitrary value only on the outer `net/http` request context is not sufficient. @@ -137,20 +137,20 @@ CodeMode capability changes do not change the outer definitions of `search_api`, ## Configuration and logging -Cobra flags take precedence over `TEMPLATE_MCP_CODEMODE_*` environment variables, which take precedence over defaults. Common commands include: +Cobra flags take precedence over `AGENTCOMPUTE_*` environment variables, which take precedence over defaults. Common commands include: ```sh -go run ./cmd/template-mcp-codemode --version -go run ./cmd/template-mcp-codemode stdio -go run ./cmd/template-mcp-codemode http --addr localhost:8080 -TEMPLATE_MCP_CODEMODE_LOG_LEVEL=debug go run ./cmd/template-mcp-codemode stdio +go run ./cmd/agentcompute --version +go run ./cmd/agentcompute stdio +go run ./cmd/agentcompute http --addr localhost:8080 +AGENTCOMPUTE_LOG_LEVEL=debug go run ./cmd/agentcompute stdio ``` -A local build reports `template-mcp-codemode dev (none) built unknown`. GoReleaser supplies version, commit, and date for releases. +A local build reports `agentcompute dev (none) built unknown`. GoReleaser supplies version, commit, and date for releases. Both transports log to stderr. STDIO reserves stdout exclusively for JSON-RPC; never write logs or diagnostics there. -CodeMode execution and discovery limits are set programmatically through `mcpserver.Options.Runtime.Limits`. The template does not add limit flags or environment variables. Zero-valued fields receive CodeMode's bounded defaults. See the [configuration reference](docs/docs/configuration.md) for the defaults and option wiring. +CodeMode execution and discovery limits are set programmatically through `mcpserver.Options.Runtime.Limits`. The server does not add limit flags or environment variables. Zero-valued fields receive CodeMode's bounded defaults. See the [configuration reference](docs/docs/configuration.md) for the defaults and option wiring. ## Common tasks @@ -178,7 +178,7 @@ The local image path builds the binary into a signed Wolfi package with [melange ```sh mise run image-local -docker run --rm template-mcp-codemode:dev --version +docker run --rm agentcompute:dev --version ``` The image runs as uid/gid 65532 and contains CA certificates and timezone data but no shell. Its default command is `http --addr 0.0.0.0:8080 --insecure` so the demonstration starts without credentials. This is intentionally unauthenticated. Remove `--insecure` and install production authentication and authorization before deployment. @@ -194,11 +194,11 @@ The configured release path is: 1. Release Please maintains a release pull request and creates a version tag plus draft GitHub release after merge. 2. The release dry-run workflow rehearses the GoReleaser binary path and the native-runner melange/apko image path on the release pull request. 3. GoReleaser builds binaries, checksums, and SBOMs without publishing directly. The release workflow validates and uploads them to the draft release. -4. Native runners build signed per-architecture Wolfi packages. apko publishes `ghcr.io/meigma/template-mcp-codemode:vX.Y.Z` as a multi-platform image. +4. Native runners build signed per-architecture Wolfi packages. apko publishes `ghcr.io/gilmanlab/agentcompute:vX.Y.Z` as a multi-platform image. 5. The isolated reusable `attest.yml` workflow creates GitHub provenance for binary checksums and the image. The release workflow also creates a keyless Cosign image signature and attaches an SBOM attestation. 6. A human inspects the draft before publication. -Before the first release from a generated project, update the release app credentials, protected-tag bypass, package names, asset patterns, image name, and `ghd.toml` signer workflow. Run the release dry-run workflow before merging that project's first release pull request. +Before the first release, supply the release app credentials, confirm protected-tag bypass for `glab-release-please`, and run the release dry-run workflow before merging the first release pull request. ## Documentation @@ -207,7 +207,7 @@ Before the first release from a generated project, update the release app creden - [Configuration](docs/docs/configuration.md) - [Security model](docs/docs/security.md) - [Canonical CodeMode documentation](https://meigma.github.io/codemode/) -- [Go API](https://pkg.go.dev/github.com/meigma/template-mcp-codemode) +- [Go API](https://pkg.go.dev/github.com/GilmanLab/agentcompute) ## Contributing diff --git a/SECURITY.md b/SECURITY.md index 655d629..0b4ee99 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Supported versions -This repository has no published release yet. Until the initial `0.1.0` release, security fixes apply to the default branch. After releases begin, only the latest released version receives security fixes; generated projects must replace this statement with their own support policy. +This repository has no published release yet. Until the initial `0.1.0` release, security fixes apply to the default branch. After releases begin, only the latest released version receives security fixes. The `0.0.0` value in release configuration is an automation baseline, not a published or supported release. diff --git a/apko.yaml b/apko.yaml index 6a1e110..d571281 100644 --- a/apko.yaml +++ b/apko.yaml @@ -1,5 +1,5 @@ # Minimal, multi-arch, nonroot runtime image assembled from the locally-built -# template-mcp-codemode apk (melange.yaml) plus the Wolfi base packages. Mirrors +# agentcompute apk (melange.yaml) plus the Wolfi base packages. Mirrors # the former gcr.io/distroless/static-debian12:nonroot posture (uid 65532, # ca-certs, tzdata, no shell). The Wolfi base intentionally floats to the latest # (fresh CA bundle/timezones, low CVE surface); the exact resolved versions are @@ -17,7 +17,7 @@ contents: - wolfi-baselayout - ca-certificates-bundle - tzdata - - template-mcp-codemode@local + - agentcompute@local # Containers are the networked deployment, so the image defaults to the http # transport bound to all interfaces. `--insecure` is required because the server @@ -25,7 +25,7 @@ contents: # real authorization before deploying. Override the args (e.g. `docker run ... stdio`) # or the whole command at runtime. entrypoint: - command: /usr/bin/template-mcp-codemode + command: /usr/bin/agentcompute cmd: http --addr 0.0.0.0:8080 --insecure # Create the nonroot user/group (uid/gid 65532) and run as it — Wolfi has no @@ -45,7 +45,7 @@ archs: - arm64 annotations: - org.opencontainers.image.title: template-mcp-codemode - org.opencontainers.image.description: Meigma CodeMode MCP server template - org.opencontainers.image.source: https://github.com/meigma/template-mcp-codemode + org.opencontainers.image.title: agentcompute + org.opencontainers.image.description: agentcompute CodeMode MCP server + org.opencontainers.image.source: https://github.com/GilmanLab/agentcompute org.opencontainers.image.version: "0.0.0" # x-release-please-version diff --git a/cmd/template-mcp-codemode/main.go b/cmd/agentcompute/main.go similarity index 94% rename from cmd/template-mcp-codemode/main.go rename to cmd/agentcompute/main.go index 6af5b43..84b5183 100644 --- a/cmd/template-mcp-codemode/main.go +++ b/cmd/agentcompute/main.go @@ -9,7 +9,7 @@ import ( "github.com/meigma/codemode" - "github.com/meigma/template-mcp-codemode/internal/cli" + "github.com/GilmanLab/agentcompute/internal/cli" ) // GoReleaser injects these values with ldflags during releases. When they are diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index a58c32b..2b84ca5 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -5,24 +5,24 @@ description: CLI flags, environment variables, CodeMode options, limits, and tra # Configuration -The CLI uses Cobra and an instance-scoped Viper configuration. Flags take precedence over environment variables, which take precedence over defaults. `internal/templateinfo.Name` derives the `TEMPLATE_MCP_CODEMODE_*` environment prefix. +The CLI uses Cobra and an instance-scoped Viper configuration. Flags take precedence over environment variables, which take precedence over defaults. `internal/templateinfo.Name` derives the `AGENTCOMPUTE_*` environment prefix. ## Commands | Command | Purpose | | --- | --- | -| `template-mcp-codemode stdio` | Serve over STDIO for a local client-launched subprocess. | -| `template-mcp-codemode http` | Serve over Streamable HTTP. | -| `template-mcp-codemode --version` | Print version, commit, and build date. | +| `agentcompute stdio` | Serve over STDIO for a local client-launched subprocess. | +| `agentcompute http` | Serve over Streamable HTTP. | +| `agentcompute --version` | Print version, commit, and build date. | -A local build prints `template-mcp-codemode dev (none) built unknown`. Release builds receive their metadata through linker flags. +A local build prints `agentcompute dev (none) built unknown`. Release builds receive their metadata through linker flags. ## Global flags | Flag | Environment | Default | Meaning | | --- | --- | --- | --- | -| `--log-level` | `TEMPLATE_MCP_CODEMODE_LOG_LEVEL` | `info` | `debug`, `info`, `warn`, or `error`. | -| `--log-format` | `TEMPLATE_MCP_CODEMODE_LOG_FORMAT` | `text` | `text` or `json`. | +| `--log-level` | `AGENTCOMPUTE_LOG_LEVEL` | `info` | `debug`, `info`, `warn`, or `error`. | +| `--log-format` | `AGENTCOMPUTE_LOG_FORMAT` | `text` | `text` or `json`. | Invalid values fail at startup. Logs always go to stderr. STDIO reserves stdout for JSON-RPC. @@ -30,9 +30,9 @@ Invalid values fail at startup. Logs always go to stderr. STDIO reserves stdout | Flag | Environment | Default | Meaning | | --- | --- | --- | --- | -| `--addr` | `TEMPLATE_MCP_CODEMODE_ADDR` | `localhost:8080` | Listen address. | -| `--auth-token` | `TEMPLATE_MCP_CODEMODE_AUTH_TOKEN` | Empty | Demonstration shared bearer token; empty disables token validation. | -| `--insecure` | `TEMPLATE_MCP_CODEMODE_INSECURE` | `false` | Permit a non-loopback bind without authentication. | +| `--addr` | `AGENTCOMPUTE_ADDR` | `localhost:8080` | Listen address. | +| `--auth-token` | `AGENTCOMPUTE_AUTH_TOKEN` | Empty | Demonstration shared bearer token; empty disables token validation. | +| `--insecure` | `AGENTCOMPUTE_INSECURE` | `false` | Permit a non-loopback bind without authentication. | A non-loopback bind without `--auth-token` fails unless `--insecure` explicitly permits unauthenticated exposure. Cross-origin protection is enabled independently of this bind check. @@ -49,9 +49,9 @@ The shared token is not a production credential system. It does not validate a s For HTTP, the SDK authentication verifier supplies a stable, non-secret `auth.TokenInfo.UserID`. The `installHTTPSubject` receiving middleware reads `req.GetExtra().TokenInfo.UserID` from each MCP request and stores an `authz.Subject` with `authz.WithSubject` on the MCP handler context. `mcpserver.ContextSubject` resolves that value. Setting a value only on the outer `net/http` request context is not sufficient because the SDK establishes the receiving handler context. MCP tool input, Starlark source, request `_meta`, and unvalidated headers are not trusted identity sources. -## Template server options +## Server options -`internal/mcpserver.New` has this template-owned API: +`internal/mcpserver.New` has this repository-owned API: ```text New(options Options) (*mcp.Server, error) @@ -106,11 +106,11 @@ srv, err := mcpserver.New(mcpserver.Options{ }) ``` -Limits are programmatic options. The template intentionally has no limit flags or `TEMPLATE_MCP_CODEMODE_*` limit variables. For exact accounting and validation rules, see the [CodeMode limits reference](https://meigma.github.io/codemode/reference/public-api/#limits). +Limits are programmatic options. The server intentionally has no limit flags or `AGENTCOMPUTE_*` limit variables. For exact accounting and validation rules, see the [CodeMode limits reference](https://meigma.github.io/codemode/reference/public-api/#limits). ## Upstream MCP adapter options -The template wrapper eventually calls the CodeMode adapter with the required three-argument signature: +The repository wrapper eventually calls the CodeMode adapter with the required three-argument signature: ```go srv, err := codemodemcp.New( @@ -133,7 +133,7 @@ The complete API is: mcpserver.New(service Service, resolver InvocationResolver, options Options) (*mcp.Server, error) ``` -The third argument is required; use `mcpserver.Options{}` to accept upstream defaults. A nil `Options.Implementation` uses implementation name `codemode` and version `2`. `Options.Logger` is optional and a nil value uses the MCP SDK default. The template supplies its own implementation name, title, version, and logger. +The third argument is required; use `mcpserver.Options{}` to accept upstream defaults. A nil `Options.Implementation` uses implementation name `codemode` and version `2`. `Options.Logger` is optional and a nil value uses the MCP SDK default. The server supplies its own implementation name, title, version, and logger. See the [canonical `mcpserver` API reference](https://meigma.github.io/codemode/reference/public-api/#mcpserver) for service, resolver, and error contracts. diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 25dcb72..8bfbe63 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -5,15 +5,15 @@ description: Run the CodeMode server and compose the demo capability. # Getting started -This tutorial starts the template as a local STDIO server, connects it to an MCP client, and composes two calls to `random.int` in one `execute` request. +This tutorial starts the server as a local STDIO process, connects it to an MCP client, and composes two calls to `random.int` in one `execute` request. ## Install the repository toolchain Clone a disposable checkout and provision the pinned Go 1.26.6 toolchain and project tools with [mise](https://mise.jdx.dev). The server module pins the official MCP Go SDK v1.7.0: ```sh -git clone https://github.com/meigma/template-mcp-codemode.git -cd template-mcp-codemode +git clone https://github.com/GilmanLab/agentcompute.git +cd agentcompute mise install ``` @@ -22,7 +22,7 @@ Moon uses the mise-provided tools as system binaries. Python and uv for the docu ## Build the server ```sh -go build -o bin/template-mcp-codemode ./cmd/template-mcp-codemode +go build -o bin/agentcompute ./cmd/agentcompute ``` The final binary contains both the ordinary server and the CodeMode worker entry point. `codemode.ServeWorkerAndExit()` is the first statement of `main`, so CodeMode can re-execute this same binary for each program run. @@ -34,8 +34,8 @@ Configure an MCP client that accepts the `mcpServers` shape. Replace the path be ```json { "mcpServers": { - "template-mcp-codemode": { - "command": "/absolute/path/to/template-mcp-codemode/bin/template-mcp-codemode", + "agentcompute": { + "command": "/absolute/path/to/agentcompute/bin/agentcompute", "args": ["stdio"] } } @@ -110,7 +110,7 @@ CodeMode runs this source in a fresh worker process. Only the final converted va Start the HTTP transport on loopback: ```sh -go run ./cmd/template-mcp-codemode http --addr localhost:8080 +go run ./cmd/agentcompute http --addr localhost:8080 ``` The server logs its listening address to stderr and shuts down gracefully on `Ctrl-C`. Loopback without a token installs the explicit non-secret development subject ID `development` in trusted request context. @@ -118,7 +118,7 @@ The server logs its listening address to stderr and shuts down gracefully on `Ct To exercise the demo bearer-token seam: ```sh -go run ./cmd/template-mcp-codemode http \ +go run ./cmd/agentcompute http \ --addr localhost:8080 \ --auth-token development-only-token ``` diff --git a/docs/docs/index.md b/docs/docs/index.md index 3394b5e..8960dd4 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,24 +1,20 @@ --- -title: template-mcp-codemode +title: agentcompute slug: / -description: A Go template for CodeMode-native Model Context Protocol servers. +description: A CodeMode-native Model Context Protocol server. --- -# template-mcp-codemode +# agentcompute -`template-mcp-codemode` is a Go template for building [Model Context Protocol](https://modelcontextprotocol.io) servers with [CodeMode](https://github.com/meigma/codemode). You register typed Go capabilities; an agent uses the fixed `search_api`, `describe_api`, and `execute` MCP tools to discover and compose them in bounded Starlark programs. +`agentcompute` is a [CodeMode](https://github.com/meigma/codemode) [Model Context Protocol](https://modelcontextprotocol.io) server. You register typed Go capabilities; an agent uses the fixed `search_api`, `describe_api`, and `execute` MCP tools to discover and compose them in bounded Starlark programs. -The template includes the `random.int` demo capability, STDIO and Streamable HTTP transports, explicit subject and authorization wiring, a hot-reload development proxy, Moon tasks, CI, documentation, and release configuration. +The repository includes the `random.int` demo capability, STDIO and Streamable HTTP transports, explicit subject and authorization wiring, a hot-reload development proxy, Moon tasks, CI, documentation, and release configuration. ## Documentation - **[Getting started](getting-started.md)** — clone the repository, run the server, and compose calls to `random.int`. - **[Add a capability](how-to/add-a-capability.md)** — add a typed Go capability and remove the demo. -- **[Configuration](configuration.md)** — CLI flags, `TEMPLATE_MCP_CODEMODE_*` environment variables, runtime options, and default limits. +- **[Configuration](configuration.md)** — CLI flags, `AGENTCOMPUTE_*` environment variables, runtime options, and default limits. - **[Security](security.md)** — trusted identity, authorization, worker isolation, cancellation, and deployment boundaries. -Use the [canonical CodeMode documentation](https://meigma.github.io/codemode/) for the complete public Go API, fixed MCP tool contracts, supported Starlark surface, and runtime security model. The template-specific Go API is published at [pkg.go.dev](https://pkg.go.dev/github.com/meigma/template-mcp-codemode). - -## Generated projects - -After creating a project from this template, follow `DELETE_ME.md`. Rename the root and proxy modules, binary, client-visible implementation identity, environment prefix, repository and image references, and documentation metadata. Preserve the CodeMode dependency and worker entry points, replace the demo with real capabilities, and reset the changelog before the first release. +Use the [canonical CodeMode documentation](https://meigma.github.io/codemode/) for the complete public Go API, fixed MCP tool contracts, supported Starlark surface, and runtime security model. The repository Go API is published at [pkg.go.dev](https://pkg.go.dev/github.com/GilmanLab/agentcompute). diff --git a/docs/docs/security.md b/docs/docs/security.md index 0ccd51d..91353f5 100644 --- a/docs/docs/security.md +++ b/docs/docs/security.md @@ -101,4 +101,4 @@ These limits do not bound handler-owned resources or impose operating-system CPU These are configured paths, not evidence that this repository has already published a release. The release baseline is `0.0.0`, with `0.1.0` pending as the first release. -Report vulnerabilities through the private process in the repository [security policy](https://github.com/meigma/template-mcp-codemode/blob/master/SECURITY.md). +Report vulnerabilities through the private process in the repository [security policy](https://github.com/GilmanLab/agentcompute/blob/master/SECURITY.md). diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index db69271..7ae14ff 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,8 +1,8 @@ -site_name: template-mcp-codemode -site_description: Meigma CodeMode MCP server template -site_url: https://meigma.github.io/template-mcp-codemode/ -repo_name: meigma/template-mcp-codemode -repo_url: https://github.com/meigma/template-mcp-codemode +site_name: agentcompute +site_description: agentcompute CodeMode MCP server +site_url: https://gilmanlab.github.io/agentcompute/ +repo_name: GilmanLab/agentcompute +repo_url: https://github.com/GilmanLab/agentcompute edit_uri: edit/master/docs/docs/ docs_dir: docs site_dir: build diff --git a/docs/moon.yml b/docs/moon.yml index 746d6b4..6956943 100644 --- a/docs/moon.yml +++ b/docs/moon.yml @@ -7,11 +7,11 @@ tags: - 'uv' project: - title: 'template-mcp-codemode docs' - description: 'MkDocs documentation site for the Meigma CodeMode MCP server template.' - owner: 'meigma' + title: 'agentcompute docs' + description: 'MkDocs documentation site for the agentcompute CodeMode MCP server.' + owner: 'GilmanLab' maintainers: - - 'meigma' + - 'GilmanLab' # uv + Python come from mise (system binaries on PATH); uv creates/activates the # project .venv itself, so moon manages no toolchain here. diff --git a/docs/pyproject.toml b/docs/pyproject.toml index ab48e79..e55e618 100644 --- a/docs/pyproject.toml +++ b/docs/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "template-mcp-codemode-docs" +name = "agentcompute-docs" version = "0.0.0" -description = "MkDocs documentation site for the Meigma CodeMode MCP server template." +description = "MkDocs documentation site for the agentcompute CodeMode MCP server." requires-python = ">=3.14" dependencies = [ "mkdocs-material>=9.7.0", diff --git a/docs/uv.lock b/docs/uv.lock index 4a4d22b..bb4671d 100644 --- a/docs/uv.lock +++ b/docs/uv.lock @@ -2,6 +2,17 @@ version = 1 revision = 3 requires-python = ">=3.14" +[[package]] +name = "agentcompute-docs" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "mkdocs-material" }, +] + +[package.metadata] +requires-dist = [{ name = "mkdocs-material", specifier = ">=9.7.0" }] + [[package]] name = "babel" version = "2.18.0" @@ -377,17 +388,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "template-mcp-codemode-docs" -version = "0.0.0" -source = { virtual = "." } -dependencies = [ - { name = "mkdocs-material" }, -] - -[package.metadata] -requires-dist = [{ name = "mkdocs-material", specifier = ">=9.7.0" }] - [[package]] name = "urllib3" version = "2.7.0" diff --git a/ghd.toml b/ghd.toml index f3c4833..d2584ad 100644 --- a/ghd.toml +++ b/ghd.toml @@ -1,32 +1,32 @@ version = 1 [provenance] -signer_workflow = "meigma/template-mcp-codemode/.github/workflows/attest.yml" +signer_workflow = "GilmanLab/agentcompute/.github/workflows/attest.yml" [[packages]] -name = "template-mcp-codemode" -description = "Meigma CodeMode MCP server template starter CLI." +name = "agentcompute" +description = "agentcompute CodeMode MCP server." tag_pattern = "v${version}" [[packages.assets]] os = "darwin" arch = "amd64" -pattern = "template-mcp-codemode_${version}_darwin_amd64" +pattern = "agentcompute_${version}_darwin_amd64" [[packages.assets]] os = "darwin" arch = "arm64" -pattern = "template-mcp-codemode_${version}_darwin_arm64" +pattern = "agentcompute_${version}_darwin_arm64" [[packages.assets]] os = "linux" arch = "amd64" -pattern = "template-mcp-codemode_${version}_linux_amd64" +pattern = "agentcompute_${version}_linux_amd64" [[packages.assets]] os = "linux" arch = "arm64" -pattern = "template-mcp-codemode_${version}_linux_arm64" +pattern = "agentcompute_${version}_linux_arm64" [[packages.binaries]] -path = "template-mcp-codemode" +path = "agentcompute" diff --git a/go.mod b/go.mod index a2f5f63..9c73cd2 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ -module github.com/meigma/template-mcp-codemode +module github.com/GilmanLab/agentcompute go 1.26.6 require ( - github.com/meigma/codemode v0.2.0 + github.com/meigma/codemode v0.2.1 github.com/modelcontextprotocol/go-sdk v1.7.0 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index 88a7015..653424f 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzH github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= -github.com/meigma/codemode v0.2.0 h1:N/QYT0PdbLT3/+/seIsE+1A0kccOsmXnknDj450dsFs= -github.com/meigma/codemode v0.2.0/go.mod h1:xZVXK9qH3apoQv3DsAldcQ0TjctDwKDN4n5dmYU0cG0= +github.com/meigma/codemode v0.2.1 h1:JrdLWuBB/rWEDtPy2y5Z5hXmtCSYOheGniaPlQhoSCQ= +github.com/meigma/codemode v0.2.1/go.mod h1:xZVXK9qH3apoQv3DsAldcQ0TjctDwKDN4n5dmYU0cG0= github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44= github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= github.com/open-policy-agent/opa v1.19.1 h1:aB1nOncChnTbQurjRQVJnjTJxditt8VqszlbaM3GGKU= diff --git a/images/README.md b/images/README.md new file mode 100644 index 0000000..3209c29 --- /dev/null +++ b/images/README.md @@ -0,0 +1,117 @@ +# images + +Lab-built Incus images for agentcompute. Phase 1 ships one: the `router` +system container (Alpine 3.22.5 with `nftables`, `frr`, `iproute2` + `tc`, +`dnsmasq`, `wireguard-tools`, `tcpdump`, nothing else). + +| Path | Role | +| --- | --- | +| `pins.yaml` | Reproducibility root: distrobuilder 3.3.1 and Go 1.26.6 source URLs + SHA-256, the Alpine minirootfs, every APK in the package closure by URL + SHA-256, the Incus client used by CI, the imgoci Go module version. | +| `router/distrobuilder.yaml` | The recipe. Installs only the pinned APKs from an offline seed (`--no-network`, empty `/etc/apk/repositories`), enables OpenRC `lxc` mode, and emits a unified tarball. | +| `build.py` | `validate` (schema and pin checks, no credentials) and `build` (download-verify, compile distrobuilder from vendored source, assemble). PEP 723 script with `build.py.lock`. | +| `catalog.yaml` | What the Go server reads: image name → GHCR reference **by digest**, kind, OS, defaults. Updated by PR after each publish. | + +## Build locally + +Linux amd64, root, `tar`/`xz`/`gcc` on `PATH`, `uv` (mise provides 0.11.0): + +```sh +uv run --locked --script images/build.py validate +mkdir -p /var/tmp/router-build +sudo env PATH="$PATH" uv run --locked --script images/build.py build \ + --work-dir /var/tmp/router-build/work --output-dir /var/tmp/router-build/out +``` + +Output: `out/router.tar.xz` and `out/metrics.json`. Both `--work-dir` and +`--output-dir` must not exist yet; their parent must. The script never +deletes anything it did not create. + +distrobuilder needs root and loop devices, not KVM. macOS cannot run it; +`sandbox01` can. + +## Publication and import + +`images-publish.yml` (protected `master` only) builds, boot-tests the tarball +in the cluster's restricted `image-build` project over the Incus API, publishes +an immutable imgoci release to `ghcr.io/gilmanlab/agentcompute/router`, fetches +it back by digest, and attests the release-index digest through the isolated +`attest.yml` workflow. The immutable tag is `tree-<12 hex>` of the `images/` +git tree, so an unchanged tree cannot be republished. + +Verify a release: + +```sh +gh attestation verify oci://ghcr.io/gilmanlab/agentcompute/router: \ + --repo GilmanLab/agentcompute \ + --signer-workflow GilmanLab/agentcompute/.github/workflows/attest.yml +``` + +Import into the cluster (throwaway tool, see `../spikes/images/README.md`): + +```sh +INCUS_CONF=... spikes/images/images import \ + --ref ghcr.io/gilmanlab/agentcompute/router@sha256: \ + --remote nas01 --project image-build +``` + +Incus does not read imgoci indexes itself; the importer resolves the digest, +verifies bytes, imports, boots, runs the six tool checks, then moves the +`router` alias. In Phase 2 this becomes the Go server's catalog reconciler. + +## imgoci representation decision + +imgoci standardizes `incus-vm` (split `metadata` + `disk`) and has no public +value for a unified Incus container tarball. The spec reserves public values +and requires producer-defined selectors to use `x--`, so the +router release uses: + +| Selector | Value | +| --- | --- | +| `io.imgoci.target` | `incus` | +| `io.imgoci.representation` | `x-gilmanlab-incus-container` | +| `io.imgoci.role` | `x-gilmanlab-unified` | +| `io.imgoci.compression` | `none` | +| `io.imgoci.architecture` | `amd64` | + +Compression is `none` because the xz wrapper is part of the Incus unified +image format (`incus image import` consumes it as-is); the content digest is +the digest of `router.tar.xz`, which equals the Incus fingerprint. The image +is not labelled `incus-vm`. Proposing a public `incus-container` +representation upstream is future work. + +## Measured builds + +Assembly on `sandbox01` (Ubuntu 26.04, 7.0 kernel, AMD Ryzen 7 UM760), each +in a fresh work directory: + +| Build | Assemble wall | Peak RSS | Scratch high-water | `router.tar.xz` | Decoded | +| --- | --- | --- | --- | --- | --- | +| 1 (hand-driven) | 0.904 s | 153,288 KiB | 159,870,976 B | 11,677,588 B | 44,462,635 B | +| 2 (hand-driven) | 0.901 s | 153,284 KiB | 159,199,232 B | 11,677,972 B | 44,462,635 B | +| 3 (hand-driven) | 0.897 s | 153,544 KiB | 169,447,424 B | 11,678,200 B | 44,462,635 B | +| `build.py` | 1.17 s | 507,696 KiB | 954,527,744 B | 11,678,052 B | 44,462,635 B | + +`build.py` numbers include the Go toolchain and vendored distrobuilder source +in the work directory (download 7.8 s, compile 31.6 s), which is why its RSS +and scratch are higher; the assembly phase itself is the ~1 s row. An earlier +first attempt failed in 0.1 s: the minirootfs's `libssl3` pinned the older +`libcrypto3`, which is why `pins.yaml` now pins the base packages together +with the router set (59 APKs, 13.2 MB downloaded). + +Scratch is sampled every 100 ms, so short peaks between samples are missed. + +### Reproducibility + +The three hand-driven tarballs decode to identical member sets, contents, +sizes, modes, and owners. Only `metadata.yaml` (`creation_date`, +`expiry_date`, stamped by distrobuilder at build time) and member mtimes +differ, so the compressed bytes and therefore the Incus fingerprint differ per +build. A CI build will not reproduce a local digest; compare decoded content, +not the fingerprint. Fixing the timestamps is deliberately not done in Phase 1. + +## Throwaway + +- `../spikes/images/` (publisher/importer and `smoke.sh`): absorbed into the + Go server's reconciler in Phase 2. +- The hand-driven build scripts used for the first three measurements are not + in the repository; `build.py` replaces them. diff --git a/images/build.py b/images/build.py new file mode 100755 index 0000000..7281cd0 --- /dev/null +++ b/images/build.py @@ -0,0 +1,528 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "PyYAML==6.0.3", +# ] +# /// +"""Validate pins and assemble the router Incus image. + +Usage: + uv run --locked --script images/build.py validate + sudo env PATH="$PATH" uv run --locked --script images/build.py build \\ + --work-dir --output-dir +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import resource +import shutil +import subprocess +import sys +import tarfile +import time +import urllib.request +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + +import yaml + +IMAGES = Path(__file__).resolve().parent +PINS_PATH = IMAGES / "pins.yaml" +RECIPE_PATH = IMAGES / "router" / "distrobuilder.yaml" +CATALOG_PATH = IMAGES / "catalog.yaml" +PYYAML_VERSION = "6.0.3" +DISTROBUILDER_TAGS = ( + "containers_image_storage_stub,containers_image_docker_daemon_stub," + "containers_image_openpgp" +) +PINS_KEYS = { + "schema_version", + "architecture", + "distrobuilder", + "go", + "incus", + "alpine", + "imgoci", + "pyyaml", +} +CATALOG_KEYS = {"schema_version", "images"} +SHA256_LEN = 64 + + +class Error(RuntimeError): + """Raised when validation or the build cannot continue.""" + + +def load_yaml(path: Path) -> Any: + if not path.is_file(): + raise Error(f"missing {path}") + try: + data = yaml.safe_load(path.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + raise Error(f"invalid YAML in {path}: {exc}") from exc + return data + + +def require_mapping(value: Any, name: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise Error(f"{name} must be a mapping") + return value + + +def require_list(value: Any, name: str) -> list[Any]: + if not isinstance(value, list): + raise Error(f"{name} must be a list") + return value + + +def require_str(value: Any, name: str) -> str: + if not isinstance(value, str) or not value: + raise Error(f"{name} must be a non-empty string") + return value + + +def require_bool(value: Any, name: str) -> bool: + if not isinstance(value, bool): + raise Error(f"{name} must be a boolean") + return value + + +def require_sha256(value: Any, name: str) -> str: + digest = require_str(value, name).lower() + if len(digest) != SHA256_LEN or any(c not in "0123456789abcdef" for c in digest): + raise Error(f"{name} must be a 64-character lowercase hex SHA-256") + return digest + + +def require_https(url: str, name: str) -> str: + parsed = urlparse(url) + if parsed.scheme != "https" or not parsed.netloc: + raise Error(f"{name} must be an https URL") + return url + + +def extra_keys(data: dict[str, Any], allowed: set[str], name: str) -> None: + extra = sorted(set(data) - allowed) + if extra: + raise Error(f"unknown keys in {name}: {', '.join(extra)}") + + +def pinned_filename(package: dict[str, Any]) -> str: + url = require_str(package.get("url"), "alpine.packages[].url") + return url.rsplit("/", 1)[-1] + + +def load_pins() -> dict[str, Any]: + pins = require_mapping(load_yaml(PINS_PATH), str(PINS_PATH)) + extra_keys(pins, PINS_KEYS, str(PINS_PATH)) + if pins.get("schema_version") != 1: + raise Error("pins.yaml schema_version must be 1") + if pins.get("architecture") != "amd64": + raise Error("pins.yaml architecture must be amd64") + for name in ("distrobuilder", "go", "incus"): + block = require_mapping(pins.get(name), name) + extra_keys(block, {"version", "url", "sha256"}, name) + require_str(block.get("version"), f"{name}.version") + require_https(require_str(block.get("url"), f"{name}.url"), f"{name}.url") + require_sha256(block.get("sha256"), f"{name}.sha256") + alpine = require_mapping(pins.get("alpine"), "alpine") + extra_keys( + alpine, + {"version", "url", "sha256", "requested", "packages", "base_packages"}, + "alpine", + ) + require_str(alpine.get("version"), "alpine.version") + require_https(require_str(alpine.get("url"), "alpine.url"), "alpine.url") + require_sha256(alpine.get("sha256"), "alpine.sha256") + requested = require_list(alpine.get("requested"), "alpine.requested") + if not all(isinstance(item, str) and item for item in requested): + raise Error("alpine.requested must be a list of non-empty strings") + packages = require_list(alpine.get("packages"), "alpine.packages") + names: set[str] = set() + for index, item in enumerate(packages): + package = require_mapping(item, f"alpine.packages[{index}]") + extra_keys(package, {"name", "version", "url", "sha256"}, f"alpine.packages[{index}]") + name = require_str(package.get("name"), f"alpine.packages[{index}].name") + if name in names: + raise Error(f"duplicate alpine package {name}") + names.add(name) + version = require_str(package.get("version"), f"alpine.packages[{index}].version") + url = require_https( + require_str(package.get("url"), f"alpine.packages[{index}].url"), + f"alpine.packages[{index}].url", + ) + require_sha256(package.get("sha256"), f"alpine.packages[{index}].sha256") + filename = url.rsplit("/", 1)[-1] + expected = f"{name}-{version}.apk" + if filename != expected: + raise Error(f"{url} filename {filename} does not match {expected}") + missing_requested = [name for name in requested if name not in names] + if missing_requested: + raise Error("alpine.requested missing from packages: " + ", ".join(missing_requested)) + base_packages = require_list(alpine.get("base_packages"), "alpine.base_packages") + if not all(isinstance(item, str) and item for item in base_packages): + raise Error("alpine.base_packages must be a list of non-empty strings") + missing_base = [name for name in base_packages if name not in names] + if missing_base: + raise Error("alpine.base_packages missing from packages: " + ", ".join(missing_base)) + imgoci = require_mapping(pins.get("imgoci"), "imgoci") + extra_keys(imgoci, {"module", "version"}, "imgoci") + require_str(imgoci.get("module"), "imgoci.module") + require_str(imgoci.get("version"), "imgoci.version") + pyyaml = require_mapping(pins.get("pyyaml"), "pyyaml") + extra_keys(pyyaml, {"version"}, "pyyaml") + if pyyaml.get("version") != PYYAML_VERSION: + raise Error(f"pyyaml.version must be {PYYAML_VERSION}") + return pins + + +def recipe_package_filenames(recipe: dict[str, Any]) -> list[str]: + packages = require_mapping(recipe.get("packages"), "recipe packages") + if packages.get("manager") != "apk": + raise Error("recipe packages.manager must be apk") + if require_bool(packages.get("update"), "packages.update"): + raise Error("recipe packages.update must be false") + filenames: list[str] = [] + for index, item in enumerate(require_list(packages.get("sets"), "packages.sets")): + package_set = require_mapping(item, f"packages.sets[{index}]") + flags = package_set.get("flags") or [] + if "--no-network" not in flags: + raise Error(f"packages.sets[{index}] must set --no-network") + for path in require_list(package_set.get("packages"), f"packages.sets[{index}].packages"): + text = require_str(path, f"packages.sets[{index}].packages[]") + prefix = "/packages/" + if not text.startswith(prefix) or text != text.strip() or "/" in text[len(prefix) :]: + raise Error(f"recipe package path must be {prefix}.apk: {text}") + filenames.append(text[len(prefix) :]) + if not filenames: + raise Error("recipe installs no packages") + return filenames + + +def load_recipe() -> dict[str, Any]: + recipe = require_mapping(load_yaml(RECIPE_PATH), str(RECIPE_PATH)) + source = require_mapping(recipe.get("source"), "source") + url = require_str(source.get("url"), "source.url") + parsed = urlparse(url) + if parsed.scheme != "file" or not parsed.path: + raise Error("recipe source.url must be a file:// seed path") + return recipe + + +def validate_package_closure(pins: dict[str, Any], recipe: dict[str, Any]) -> None: + pinned = [pinned_filename(package) for package in pins["alpine"]["packages"]] + recipe_files = recipe_package_filenames(recipe) + if sorted(pinned) != sorted(recipe_files): + only_pins = sorted(set(pinned) - set(recipe_files)) + only_recipe = sorted(set(recipe_files) - set(pinned)) + details = [] + if only_pins: + details.append("in pins only: " + ", ".join(only_pins)) + if only_recipe: + details.append("in recipe only: " + ", ".join(only_recipe)) + raise Error("package closure mismatch: " + "; ".join(details)) + if len(set(recipe_files)) != len(recipe_files): + raise Error("recipe package list has duplicates") + + +def validate_catalog() -> None: + if not CATALOG_PATH.exists(): + return + catalog = require_mapping(load_yaml(CATALOG_PATH), str(CATALOG_PATH)) + extra_keys(catalog, CATALOG_KEYS, str(CATALOG_PATH)) + if "images" not in catalog: + raise Error("catalog.yaml must define images") + images = catalog.get("images") + if not isinstance(images, (list, dict)): + raise Error("catalog.yaml images must be a list or mapping") + + +def validate() -> dict[str, Any]: + pins = load_pins() + recipe = load_recipe() + validate_package_closure(pins, recipe) + validate_catalog() + return pins + + +def require_new_dir(path: Path, name: str) -> Path: + resolved = path.expanduser() + if not resolved.is_absolute(): + resolved = Path.cwd() / resolved + resolved = resolved.resolve(strict=False) + if resolved.exists(): + raise Error(f"{name} already exists: {resolved}") + parent = resolved.parent + if not parent.is_dir(): + raise Error(f"{name} parent does not exist: {parent}") + resolved.mkdir(mode=0o700) + resolved.chmod(0o700) + return resolved + + +def sha256_file(path: Path) -> str: + hasher = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + hasher.update(chunk) + return hasher.hexdigest() + + +def download(url: str, dest: Path, digest: str) -> None: + hasher = hashlib.sha256() + partial = dest.with_name(dest.name + ".partial") + request = urllib.request.Request(url, method="GET") + try: + with urllib.request.urlopen(request) as response, partial.open("wb") as out: + while True: + chunk = response.read(1024 * 1024) + if not chunk: + break + hasher.update(chunk) + out.write(chunk) + except OSError as exc: + partial.unlink(missing_ok=True) + raise Error(f"download failed for {url}: {exc}") from exc + got = hasher.hexdigest() + if got != digest: + partial.unlink(missing_ok=True) + raise Error(f"sha256 mismatch for {url}: got {got} want {digest}") + partial.replace(dest) + + +def run_checked(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: + try: + return subprocess.run(command, check=True, text=True, **kwargs) + except FileNotFoundError as exc: + raise Error(f"missing command: {command[0]}") from exc + except subprocess.CalledProcessError as exc: + detail = (exc.stderr or exc.stdout or "").strip() + suffix = f"\n{detail}" if detail else "" + raise Error(f"command failed: {' '.join(command)}{suffix}") from exc + + +def first_line(command: list[str]) -> str: + try: + result = subprocess.run(command, check=True, text=True, capture_output=True) + except (FileNotFoundError, subprocess.CalledProcessError) as exc: + raise Error(f"unable to read version from {command[0]}") from exc + line = (result.stdout or result.stderr).splitlines() + if not line: + raise Error(f"empty version output from {command[0]}") + return line[0] + + +def scratch_bytes(root: Path) -> int: + total = 0 + for dirpath, _dirnames, filenames in os.walk(root, followlinks=False): + for name in filenames: + path = os.path.join(dirpath, name) + try: + total += os.lstat(path).st_blocks * 512 + except OSError: + continue + return total + + +def rss_kib() -> int: + return int(resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss) + + +def extract_tar(archive: Path, dest: Path) -> None: + dest.mkdir(mode=0o700, exist_ok=True) + dest.chmod(0o700) + run_checked(["tar", "-xzf", str(archive), "-C", str(dest)]) + + +def build(work_dir: Path, output_dir: Path) -> dict[str, Any]: + pins = validate() + info = os.uname() + if info.sysname != "Linux" or info.machine not in {"x86_64", "amd64"}: + raise Error(f"build requires Linux amd64, not {info.sysname} {info.machine}") + if os.geteuid() != 0: + raise Error("build requires root") + if work_dir.expanduser().resolve(strict=False) == output_dir.expanduser().resolve(strict=False): + raise Error("work-dir and output-dir must be different") + os.umask(0o077) + work = require_new_dir(work_dir, "--work-dir") + output = require_new_dir(output_dir, "--output-dir") + tools = { + "tar": first_line(["tar", "--version"]), + "xz": first_line(["xz", "--version"]), + "gcc": first_line(["gcc", "--version"]), + } + downloads = work / "downloads" + packages_dir = work / "packages" + downloads.mkdir(mode=0o700) + packages_dir.mkdir(mode=0o700) + + download_started = time.monotonic() + go_archive = downloads / pins["go"]["url"].rsplit("/", 1)[-1] + distro_archive = downloads / pins["distrobuilder"]["url"].rsplit("/", 1)[-1] + miniroot_archive = downloads / pins["alpine"]["url"].rsplit("/", 1)[-1] + download(pins["go"]["url"], go_archive, pins["go"]["sha256"]) + download(pins["distrobuilder"]["url"], distro_archive, pins["distrobuilder"]["sha256"]) + download(pins["alpine"]["url"], miniroot_archive, pins["alpine"]["sha256"]) + apk_files: list[Path] = [] + for package in pins["alpine"]["packages"]: + filename = pinned_filename(package) + dest = packages_dir / filename + download(package["url"], dest, package["sha256"]) + apk_files.append(dest) + download_wall = round(time.monotonic() - download_started, 3) + + compile_started = time.monotonic() + extract_tar(go_archive, work) + extract_tar(distro_archive, work) + go_bin = work / "go" / "bin" + distro_src = work / f"distrobuilder-{pins['distrobuilder']['version']}" + distro_bin = work / "distrobuilder" + if not (go_bin / "go").is_file(): + raise Error("go toolchain extract did not produce go/bin/go") + if not distro_src.is_dir(): + raise Error(f"distrobuilder extract did not produce {distro_src.name}") + env = os.environ.copy() + env["PATH"] = f"{go_bin}{os.pathsep}{env.get('PATH', '')}" + env["GOTOOLCHAIN"] = "local" + env["GOCACHE"] = str(work / "gocache") + env["GOMODCACHE"] = str(work / "gomod") + (work / "gocache").mkdir(mode=0o700) + (work / "gomod").mkdir(mode=0o700) + run_checked( + [ + str(go_bin / "go"), + "build", + "-mod=vendor", + "-trimpath", + f"-tags={DISTROBUILDER_TAGS}", + "-o", + str(distro_bin), + "./distrobuilder", + ], + cwd=distro_src, + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + ) + if not distro_bin.is_file(): + raise Error("distrobuilder compile did not produce a binary") + compile_wall = round(time.monotonic() - compile_started, 3) + compile_rss = rss_kib() + + seed = work / "seed" + seed.mkdir(mode=0o700) + run_checked(["tar", "-xzf", str(miniroot_archive), "-C", str(seed)]) + seed_packages = seed / "packages" + seed_packages.mkdir(mode=0o700) + for apk in apk_files: + target = seed_packages / apk.name + try: + os.link(apk, target) + except OSError: + shutil.copyfile(apk, target, follow_symlinks=False) + if sha256_file(target) != sha256_file(apk): + raise Error(f"seed copy changed bytes: {apk.name}") + seed_tar = work / "seed.tar" + run_checked(["tar", "-cf", str(seed_tar), "-C", str(seed), "."]) + shutil.rmtree(seed) + + cache = work / "cache" + cache.mkdir(mode=0o700) + command = [ + str(distro_bin), + "build-incus", + str(RECIPE_PATH), + str(output), + "--type=unified", + "--compression=xz-1", + "--disable-overlay", + f"--cache-dir={cache}", + "-o", + f"source.url={seed_tar.resolve().as_uri()}", + ] + assemble_started = time.monotonic() + peak_scratch = scratch_bytes(work) + log_path = work / "build.log" + with log_path.open("w", encoding="utf-8") as log: + process = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT) + while process.poll() is None: + peak_scratch = max(peak_scratch, scratch_bytes(work)) + time.sleep(0.1) + peak_scratch = max(peak_scratch, scratch_bytes(work)) + assemble_wall = round(time.monotonic() - assemble_started, 3) + assemble_rss = rss_kib() + if process.returncode: + sys.stderr.write(log_path.read_text(encoding="utf-8", errors="replace")) + raise Error(f"distrobuilder exited {process.returncode}") + + artifact = output / "router.tar.xz" + if not artifact.is_file(): + raise Error(f"missing {artifact}") + try: + with tarfile.open(artifact) as archive: + decoded = sum(member.size for member in archive) + except tarfile.TarError as exc: + raise Error(f"invalid unified image {artifact}: {exc}") from exc + if decoded <= 0: + raise Error(f"{artifact} decoded to 0 bytes") + metrics = { + "download_wall_seconds": download_wall, + "compile_wall_seconds": compile_wall, + "assemble_wall_seconds": assemble_wall, + "download_includes": ( + "https fetch and sha256 of go, vendored distrobuilder source, " + "alpine miniroot, and every pinned apk" + ), + "compile_includes": ( + "extract go+distrobuilder and go build -mod=vendor " + f"-tags={DISTROBUILDER_TAGS}; excludes download and assemble" + ), + "assemble_includes": ( + "seed tar plus distrobuilder build-incus --type=unified " + "--compression=xz-1 --disable-overlay; excludes download and compile" + ), + "peak_rss_kib": assemble_rss, + "compile_peak_rss_kib": compile_rss, + "scratch_high_water_bytes": peak_scratch, + "scratch_sampling_seconds": 0.1, + "artifact_bytes": artifact.stat().st_size, + "artifact_sha256": sha256_file(artifact), + "decoded_bytes": decoded, + "tools": tools, + } + (output / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n", encoding="utf-8") + print(json.dumps(metrics, indent=2)) + return metrics + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + sub.add_parser("validate", help="check pins, recipe closure, and catalog keys") + build_cmd = sub.add_parser("build", help="download, compile, and assemble router.tar.xz") + build_cmd.add_argument("--work-dir", type=Path, required=True) + build_cmd.add_argument("--output-dir", type=Path, required=True) + return parser.parse_args(argv) + + +def main(argv: list[str]) -> int: + try: + args = parse_args(argv) + if args.command == "validate": + validate() + else: + build(args.work_dir, args.output_dir) + except Error as exc: + print(f"images/build.py: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/images/build.py.lock b/images/build.py.lock new file mode 100644 index 0000000..552fb6f --- /dev/null +++ b/images/build.py.lock @@ -0,0 +1,61 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[manifest] +requirements = [{ name = "pyyaml", specifier = "==6.0.3" }] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] diff --git a/images/pins.yaml b/images/pins.yaml new file mode 100644 index 0000000..ca00f15 --- /dev/null +++ b/images/pins.yaml @@ -0,0 +1,287 @@ +schema_version: 1 +architecture: amd64 +distrobuilder: + version: 3.3.1 + url: https://github.com/lxc/distrobuilder/releases/download/v3.3.1/distrobuilder-3.3.1.tar.gz + sha256: 6c411af7178bb55ef649c708f4f38fc3c30e6ecce901c08d8a389448a900a73a +go: + version: 1.26.6 + url: https://go.dev/dl/go1.26.6.linux-amd64.tar.gz + sha256: 708effb774be8237570d0add163225abbdfaf4fca28b2611df167beba4feef89 +alpine: + version: 3.22.5 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/x86_64/alpine-minirootfs-3.22.5-x86_64.tar.gz + sha256: 4b4daa9fe2fc696c4919c4412a4c3d3e770d8fb70292a004a2c72f5096175282 + requested: + - openrc + - busybox-openrc + - nftables + - frr + - iproute2 + - iproute2-tc + - dnsmasq + - wireguard-tools + - tcpdump + packages: + - name: alpine-baselayout + version: 3.7.0-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/alpine-baselayout-3.7.0-r0.apk + sha256: 610c9c4627c27d2ee8def1847f0c29bbbb770ab264ff8bb0f875ccfba53cc33c + - name: alpine-baselayout-data + version: 3.7.0-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/alpine-baselayout-data-3.7.0-r0.apk + sha256: 49a9c9b4ff386c3022a52c999f3becdce680ae3b1ee716b041c499d75e07c68c + - name: alpine-keys + version: 2.5-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/alpine-keys-2.5-r0.apk + sha256: 1069fa68769607690e46b0d689f1ad9b5e346be2752ece313685b4f29ec70e25 + - name: alpine-release + version: 3.22.5-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/alpine-release-3.22.5-r0.apk + sha256: 0b38cc5b0c5ba92cd62e11ff4db17d3877b94c31fa42582cc1180739edbf8e65 + - name: apk-tools + version: 2.14.10-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/apk-tools-2.14.10-r0.apk + sha256: 6d915a6ec2682fda0e850569919ddcb094d8def4ac52a94f2e7371cde06db218 + - name: bash + version: 5.2.37-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/bash-5.2.37-r0.apk + sha256: 8218982214f1b51f520d282b0c81d654846e974ce009e758acc6187bd64fe228 + - name: bridge + version: 1.5-r5 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/bridge-1.5-r5.apk + sha256: 1c488784e57dd8c9e8b6a33c6ec163cbe1fe6599fff9a628e1afacb96b006419 + - name: busybox + version: 1.37.0-r20 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/busybox-1.37.0-r20.apk + sha256: 7cc34c2dda2520b456ab4a55abc37fe245ac90f98ab9f1c7a9be3aec035df48e + - name: busybox-binsh + version: 1.37.0-r20 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/busybox-binsh-1.37.0-r20.apk + sha256: ad33d8d799ed80eab45bb68d63c14062165d3653f4fb52e73959cd31d032210e + - name: busybox-openrc + version: 1.37.0-r20 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/busybox-openrc-1.37.0-r20.apk + sha256: ce506a835109c37ceaf401244d4dddd05390bba487812e191f8f58580ba6c347 + - name: c-ares + version: 1.34.8-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/c-ares-1.34.8-r0.apk + sha256: 1397ec9682ff6153e5d037965c76408e570ae6535ce479cc2af37436fdea52ce + - name: ca-certificates-bundle + version: 20260611-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/ca-certificates-bundle-20260611-r0.apk + sha256: a18fd1bd8bea03966ee5719aa61e44d9a810db2c8b6641b45f92b30e860f0927 + - name: dnsmasq + version: 2.91-r1 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/dnsmasq-2.91-r1.apk + sha256: ea0cdcf3fabad410f961a3b70c51d04a1d43d945811880b04698a44a948410d5 + - name: dnsmasq-common + version: 2.91-r1 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/dnsmasq-common-2.91-r1.apk + sha256: 7644492f2669d19bf4177cafe15c09c4cbad6f170780711330e665c7b6951b89 + - name: dnsmasq-openrc + version: 2.91-r1 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/dnsmasq-openrc-2.91-r1.apk + sha256: 3c919c242075b85cbf5e1e0606b792e47d0932780fa77b0115ccfb32ee783652 + - name: frr + version: 10.2.2-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/community/x86_64/frr-10.2.2-r0.apk + sha256: d68886b3a585304ea915f700fd15c253f894257df5c3cc8c486d78365e6a7632 + - name: frr-openrc + version: 10.2.2-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/community/x86_64/frr-openrc-10.2.2-r0.apk + sha256: 9f87e079df619cef044569d932d96f17d9dce82dcde029167539828e3122a323 + - name: gmp + version: 6.3.0-r3 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/gmp-6.3.0-r3.apk + sha256: d3f987ae3836ac7774324bff443dd49d03b846209660729d0c30dfff5546e138 + - name: ifupdown-ng + version: 0.12.1-r7 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/ifupdown-ng-0.12.1-r7.apk + sha256: 3c9bcdf2fa59e8f27ae429809e9ed6c3f08aca7b1b9f3e7b3756b2e5d9ebec92 + - name: ifupdown-ng-iproute2 + version: 0.12.1-r7 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/ifupdown-ng-iproute2-0.12.1-r7.apk + sha256: b1479f85f3fe30b3b02e74c55eee30a1fe41643d0b2ae3206b48b4c79b99f979 + - name: ifupdown-ng-wireguard + version: 0.12.1-r7 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/ifupdown-ng-wireguard-0.12.1-r7.apk + sha256: 378e89663cb1a47a0368821d67d67091f493822913211bae8ddd43977bd9275f + - name: ifupdown-ng-wireguard-quick + version: 0.12.1-r7 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/ifupdown-ng-wireguard-quick-0.12.1-r7.apk + sha256: 84ced4638b3074ea843674241345db0c2890efe30010a7410807b81999b2af85 + - name: iproute2 + version: 6.15.0-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/iproute2-6.15.0-r0.apk + sha256: 40839347a6314307a74da9013f44fbf652e4790752d53268e637a40d45f3274b + - name: iproute2-minimal + version: 6.15.0-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/iproute2-minimal-6.15.0-r0.apk + sha256: fb761fe0df9d23926d20d9ef5d5ca3a6ba08338b16ce26beb02305eba5733382 + - name: iproute2-ss + version: 6.15.0-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/iproute2-ss-6.15.0-r0.apk + sha256: 29a1bddb4c469fb03caee166876b3cb6d2993edef7d508ae0612719fc4fc06c6 + - name: iproute2-tc + version: 6.15.0-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/iproute2-tc-6.15.0-r0.apk + sha256: 8dbd32a1be3096e8d406e35b2eab72983eb8e21a4469480ad478803359729a37 + - name: jansson + version: 2.14.1-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/jansson-2.14.1-r0.apk + sha256: 7fde81421482507163410715a7ef7d7df6a091870edab855c24e8f6fc84e3e2d + - name: json-c + version: 0.18-r1 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/json-c-0.18-r1.apk + sha256: 91d3efdc4577d724e185ad1d655f32437c788533891788885d4267c7bdb84d97 + - name: libapk2 + version: 2.14.10-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/libapk2-2.14.10-r0.apk + sha256: b7b8ccfeaf2396cf022e685b11365be682b3b97c074d4e2a9d73c67f5beec07d + - name: libcap2 + version: 2.78-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/libcap2-2.78-r0.apk + sha256: 9850759bbb16f1ff6d1a49dc99947ef1401e1c10e5ca24f8380e69ba19f077c9 + - name: libcrypto3 + version: 3.5.8-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/libcrypto3-3.5.8-r0.apk + sha256: 1d111bc0ad6380fdda22e6513941dc2e7988d6b1621d535bed6b3fa5ef086fae + - name: libelf + version: 0.193-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/libelf-0.193-r0.apk + sha256: b8df03bd86f172aa1741790592056e6d6588247ddf6473920fa32cd7cc681f6a + - name: libmnl + version: 1.0.5-r2 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/libmnl-1.0.5-r2.apk + sha256: e9dc63c95a0c8a263dc7f0705e6f7a2220d632a675ce85db798d33a40b1c1b0b + - name: libncursesw + version: 6.5_p20250503-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/libncursesw-6.5_p20250503-r0.apk + sha256: aeafdfca68147b014705b4e2564639ade6345198debb522f2c6c51d32e417651 + - name: libnftnl + version: 1.2.9-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/libnftnl-1.2.9-r0.apk + sha256: bef674635aec00dca296b8206751245f7664ba10c416f31c01a563af596720ae + - name: libpcap + version: 1.10.7-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/libpcap-1.10.7-r0.apk + sha256: 3d2b8ad2eef3e04dfb55f4d28b673543eaf59df0cbfb7a0dbce8f88c315cdd50 + - name: libssl3 + version: 3.5.8-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/libssl3-3.5.8-r0.apk + sha256: e8d3ea5e9750cb1f4c4b459d172630925fe6bc13c113c590ac36c78013821e62 + - name: libxtables + version: 1.8.11-r1 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/libxtables-1.8.11-r1.apk + sha256: 5367f1f5c309a0aeede0a08d73af717f582f7a135ff97080aa0e5b7c72fd97af + - name: libyang + version: 2.1.148-r1 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/community/x86_64/libyang-2.1.148-r1.apk + sha256: 0b71291a0c5068fbf7fd4ff09b7a28eeae04e7d23321c888642af05446c75ecd + - name: musl + version: 1.2.5-r12 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/musl-1.2.5-r12.apk + sha256: 4990a5e0ba312e478f94cfe431a70efef1538004eb361c8ae424516848be45bb + - name: musl-utils + version: 1.2.5-r12 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/musl-utils-1.2.5-r12.apk + sha256: ee17c4904a3fbbee1e7451465ad5e61c16a3dc6f38dba6c8d61f1bc0e67ee31d + - name: ncurses-terminfo-base + version: 6.5_p20250503-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/ncurses-terminfo-base-6.5_p20250503-r0.apk + sha256: 0815a5f0403974bb9c34d456e71dc9c0222cb5455d393bc63c44b573da3d7fe0 + - name: nftables + version: 1.1.3-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/nftables-1.1.3-r0.apk + sha256: 96bdef738b0ae22ad86500af3345622c7f5bdc6ade0a407d087d1ea3223c8bc7 + - name: nftables-openrc + version: 1.1.3-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/nftables-openrc-1.1.3-r0.apk + sha256: ea553c4d7f56718e47ce740681c6365d3485ff7bded24f251986e0ad65a9083f + - name: openrc + version: 0.62.6-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/openrc-0.62.6-r0.apk + sha256: 8af76f1800c91b24174569ebf372c438eb63328513dd720b261bd9939717ed11 + - name: openrc-user + version: 0.62.6-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/openrc-user-0.62.6-r0.apk + sha256: a63ef1de12544cdda8e198cbff58c98b3b318599d7cabba3c0b43b92f527cc95 + - name: openresolv + version: 3.14.1-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/openresolv-3.14.1-r0.apk + sha256: 898fa692704121b8028a50d120e11add170ac1810593a1247a89e510ae49d556 + - name: pcre2 + version: 10.46-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/pcre2-10.46-r0.apk + sha256: cfb8ad103a101fa6a31769e50e188dab9c60124705682d01b3de268795db58ad + - name: protobuf-c + version: 1.5.2-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/protobuf-c-1.5.2-r0.apk + sha256: be89a0a08f2c1827251dc84dffbf4b687aac0b41d437249758c31671b6010c59 + - name: readline + version: 8.2.13-r1 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/readline-8.2.13-r1.apk + sha256: 520fa586c689144928191bee13e2c85ff4e170ad87d1471ec48e3e97611673d8 + - name: scanelf + version: 1.3.8-r1 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/scanelf-1.3.8-r1.apk + sha256: 8763270cf55b2a38dae0cea774e8dcab547b710fa59e03054924d61a5eba609e + - name: ssl_client + version: 1.37.0-r20 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/ssl_client-1.37.0-r20.apk + sha256: 11b2a5f91caf8eb5daa9a0bc2586f93545de62897f3a704fd49533e1654b7a68 + - name: tcpdump + version: 4.99.5-r1 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/tcpdump-4.99.5-r1.apk + sha256: 12bb5c3ad8a3d4b81f9a14f6afcd17b188f687cd2a3a432f8e3053cca1ab2933 + - name: wireguard-tools + version: 1.0.20250521-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/wireguard-tools-1.0.20250521-r0.apk + sha256: 083231296e619a860cbe6ad5582a75f92262a8276a363db19a1580a27c69b3fd + - name: wireguard-tools-openrc + version: 1.0.20250521-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/wireguard-tools-openrc-1.0.20250521-r0.apk + sha256: 543d0eeebc256a3496a6145bd10f85d6bd42c1d73300190768da0672a0bf6e40 + - name: wireguard-tools-wg + version: 1.0.20250521-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/wireguard-tools-wg-1.0.20250521-r0.apk + sha256: 709f4a9823862ddd57b7e1ab31ae247dabe4c448a11980fb4c86e5970edb05eb + - name: wireguard-tools-wg-quick + version: 1.0.20250521-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/wireguard-tools-wg-quick-1.0.20250521-r0.apk + sha256: 105232f048e4887355acdde9d693d8729ad2f171e4d6e283f139d7d5525b1da9 + - name: zlib + version: 1.3.2-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/zlib-1.3.2-r0.apk + sha256: 1f3d5f463f490dad3a68097376711bfe5e8156e9e8daff3070513aa4378cdeca + - name: zstd-libs + version: 1.5.7-r0 + url: https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/zstd-libs-1.5.7-r0.apk + sha256: 1bdd6e57cfbfbfd6e8481cad37ddd5d199950715bec1879b3afb600272dbb09e + base_packages: + - alpine-baselayout + - alpine-baselayout-data + - alpine-keys + - alpine-release + - apk-tools + - busybox + - busybox-binsh + - ca-certificates-bundle + - libapk2 + - libcrypto3 + - libssl3 + - musl + - musl-utils + - scanelf + - ssl_client + - zlib +imgoci: + module: github.com/imgoci/go + version: v0.1.0 +incus: + version: 7.4.0 + url: https://github.com/lxc/incus/releases/download/v7.4.0/bin.linux.incus.x86_64 + sha256: 2fa9b2699672225dde5290962f4087b426b5ecbd4fed0df8e430687d3b76b500 +pyyaml: + version: 6.0.3 diff --git a/images/router/distrobuilder.yaml b/images/router/distrobuilder.yaml new file mode 100644 index 0000000..73242b6 --- /dev/null +++ b/images/router/distrobuilder.yaml @@ -0,0 +1,141 @@ +image: + distribution: alpinelinux + release: 3.22.5 + architecture: x86_64 + name: router + description: Router system container (Alpine 3.22.5) + variant: router + serial: phase1 + expiry: 3650d +source: + downloader: rootfs-http + url: file:///build/seed.tar +packages: + manager: apk + update: false + cleanup: true + sets: + - action: install + flags: + - --no-network + packages: + - /packages/alpine-baselayout-3.7.0-r0.apk + - /packages/alpine-baselayout-data-3.7.0-r0.apk + - /packages/alpine-keys-2.5-r0.apk + - /packages/alpine-release-3.22.5-r0.apk + - /packages/apk-tools-2.14.10-r0.apk + - /packages/bash-5.2.37-r0.apk + - /packages/bridge-1.5-r5.apk + - /packages/busybox-1.37.0-r20.apk + - /packages/busybox-binsh-1.37.0-r20.apk + - /packages/busybox-openrc-1.37.0-r20.apk + - /packages/c-ares-1.34.8-r0.apk + - /packages/ca-certificates-bundle-20260611-r0.apk + - /packages/dnsmasq-2.91-r1.apk + - /packages/dnsmasq-common-2.91-r1.apk + - /packages/dnsmasq-openrc-2.91-r1.apk + - /packages/frr-10.2.2-r0.apk + - /packages/frr-openrc-10.2.2-r0.apk + - /packages/gmp-6.3.0-r3.apk + - /packages/ifupdown-ng-0.12.1-r7.apk + - /packages/ifupdown-ng-iproute2-0.12.1-r7.apk + - /packages/ifupdown-ng-wireguard-0.12.1-r7.apk + - /packages/ifupdown-ng-wireguard-quick-0.12.1-r7.apk + - /packages/iproute2-6.15.0-r0.apk + - /packages/iproute2-minimal-6.15.0-r0.apk + - /packages/iproute2-ss-6.15.0-r0.apk + - /packages/iproute2-tc-6.15.0-r0.apk + - /packages/jansson-2.14.1-r0.apk + - /packages/json-c-0.18-r1.apk + - /packages/libapk2-2.14.10-r0.apk + - /packages/libcap2-2.78-r0.apk + - /packages/libcrypto3-3.5.8-r0.apk + - /packages/libelf-0.193-r0.apk + - /packages/libmnl-1.0.5-r2.apk + - /packages/libncursesw-6.5_p20250503-r0.apk + - /packages/libnftnl-1.2.9-r0.apk + - /packages/libpcap-1.10.7-r0.apk + - /packages/libssl3-3.5.8-r0.apk + - /packages/libxtables-1.8.11-r1.apk + - /packages/libyang-2.1.148-r1.apk + - /packages/musl-1.2.5-r12.apk + - /packages/musl-utils-1.2.5-r12.apk + - /packages/ncurses-terminfo-base-6.5_p20250503-r0.apk + - /packages/nftables-1.1.3-r0.apk + - /packages/nftables-openrc-1.1.3-r0.apk + - /packages/openrc-0.62.6-r0.apk + - /packages/openrc-user-0.62.6-r0.apk + - /packages/openresolv-3.14.1-r0.apk + - /packages/pcre2-10.46-r0.apk + - /packages/protobuf-c-1.5.2-r0.apk + - /packages/readline-8.2.13-r1.apk + - /packages/scanelf-1.3.8-r1.apk + - /packages/ssl_client-1.37.0-r20.apk + - /packages/tcpdump-4.99.5-r1.apk + - /packages/wireguard-tools-1.0.20250521-r0.apk + - /packages/wireguard-tools-openrc-1.0.20250521-r0.apk + - /packages/wireguard-tools-wg-1.0.20250521-r0.apk + - /packages/wireguard-tools-wg-quick-1.0.20250521-r0.apk + - /packages/zlib-1.3.2-r0.apk + - /packages/zstd-libs-1.5.7-r0.apk +files: +- path: /etc/hostname + generator: hostname +- path: /etc/hosts + generator: hosts +- path: /etc/network/interfaces + generator: dump + content: 'auto lo + + iface lo inet loopback + + + auto eth0 + + iface eth0 inet dhcp + + ' +- path: /etc/inittab + generator: dump + content: '::sysinit:/sbin/openrc sysinit + + ::sysinit:/sbin/openrc boot + + ::wait:/sbin/openrc default + + ::respawn:/sbin/getty 38400 console + + ::ctrlaltdel:/sbin/reboot + + ::shutdown:/sbin/openrc shutdown + + ' +actions: +- trigger: post-unpack + action: '#!/bin/sh + + set -eu + + : > /etc/apk/repositories + + ' +- trigger: post-packages + action: '#!/bin/sh + + set -eu + + printf ''rc_sys="lxc"\n'' >> /etc/rc.conf + + rc-update add devfs sysinit + + rc-update add bootmisc boot + + rc-update add hostname boot + + rc-update add networking default + + rm -rf /packages + + ' +mappings: + architecture_map: alpinelinux diff --git a/internal/cli/http.go b/internal/cli/http.go index 24b637a..2887ea1 100644 --- a/internal/cli/http.go +++ b/internal/cli/http.go @@ -18,7 +18,7 @@ import ( "github.com/meigma/codemode/authz" hostmcp "github.com/meigma/codemode/mcpserver" - "github.com/meigma/template-mcp-codemode/internal/templateinfo" + "github.com/GilmanLab/agentcompute/internal/templateinfo" ) const ( diff --git a/internal/cli/http_identity_test.go b/internal/cli/http_identity_test.go index 619385e..e56e055 100644 --- a/internal/cli/http_identity_test.go +++ b/internal/cli/http_identity_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/meigma/template-mcp-codemode/internal/mcpserver" + "github.com/GilmanLab/agentcompute/internal/mcpserver" ) // alicePolicy allows only the authenticated alice identity, irrespective of program metadata. diff --git a/internal/cli/http_test.go b/internal/cli/http_test.go index de66361..3167c3c 100644 --- a/internal/cli/http_test.go +++ b/internal/cli/http_test.go @@ -17,7 +17,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/meigma/template-mcp-codemode/internal/templateinfo" + "github.com/GilmanLab/agentcompute/internal/templateinfo" ) // serverExitTimeout bounds how long tests wait for a serving function to @@ -175,7 +175,7 @@ func TestServeHTTPShutsDownOnContextCancel(t *testing.T) { } // TestHTTPCommandReadsAddrFromEnvironment exercises the -// TEMPLATE_MCP_CODEMODE_ADDR -> addr binding (the wiring most likely to break +// AGENTCOMPUTE_ADDR -> addr binding (the wiring most likely to break // silently after the rename step). The fail-closed guard refuses the // non-loopback address before any socket is bound, so the refusal error // mentioning that address proves the env value reached the command. @@ -193,7 +193,7 @@ func TestHTTPCommandReadsAddrFromEnvironment(t *testing.T) { } // TestEnvBindingResolvesHyphenatedFlag covers the SetEnvKeyReplacer hop that the -// addr test does not: the "auth-token" flag binds to TEMPLATE_MCP_CODEMODE_AUTH_TOKEN +// addr test does not: the "auth-token" flag binds to AGENTCOMPUTE_AUTH_TOKEN // (hyphen -> underscore). A regression dropping the replacer would break this // while the hyphen-free addr key kept working, so it is tested explicitly. It // binds flags directly rather than serving, keeping the test deterministic. diff --git a/internal/cli/root.go b/internal/cli/root.go index d4ffbfa..bd33765 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -1,4 +1,4 @@ -// Package cli builds the template-mcp-codemode command tree. +// Package cli builds the agentcompute command tree. // // The root command wires two transport subcommands onto the same // transport-agnostic MCP server from internal/mcpserver: stdio, for clients @@ -24,8 +24,8 @@ import ( "github.com/meigma/codemode/authz" hostmcp "github.com/meigma/codemode/mcpserver" - "github.com/meigma/template-mcp-codemode/internal/mcpserver" - "github.com/meigma/template-mcp-codemode/internal/templateinfo" + "github.com/GilmanLab/agentcompute/internal/mcpserver" + "github.com/GilmanLab/agentcompute/internal/templateinfo" ) // BuildInfo describes linker-injected build metadata printed by --version. @@ -53,11 +53,11 @@ type Options struct { Build BuildInfo // Viper is the configuration instance used by the command tree. Flags are // bound to environment variables named after [templateinfo.EnvPrefix], - // for example TEMPLATE_MCP_CODEMODE_ADDR. + // for example AGENTCOMPUTE_ADDR. Viper *viper.Viper } -// NewRootCommand creates the template-mcp-codemode Cobra command tree. +// NewRootCommand creates the agentcompute Cobra command tree. // // The root command does no work on its own; it wires the two transport // subcommands (stdio and http) onto the same MCP server. To produce a @@ -102,7 +102,7 @@ func NewRootCommand(options Options) *cobra.Command { root.SetErr(options.Err) // Persistent logging flags apply to every subcommand and bind to - // TEMPLATE_MCP_CODEMODE_LOG_LEVEL / TEMPLATE_MCP_CODEMODE_LOG_FORMAT via + // AGENTCOMPUTE_LOG_LEVEL / AGENTCOMPUTE_LOG_FORMAT via // initializeConfig. Logs always go to stderr; stdout stays the JSON-RPC // channel. root.PersistentFlags().String( diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index fbac44d..b9150b1 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -30,7 +30,7 @@ func TestVersionFlagPrintsBuildMetadata(t *testing.T) { err := root.ExecuteContext(context.Background()) require.NoError(t, err) - assert.Equal(t, "template-mcp-codemode 0.1.0 (abc1234) built 2026-05-08T10:00:00Z\n", stdout.String()) + assert.Equal(t, "agentcompute 0.1.0 (abc1234) built 2026-05-08T10:00:00Z\n", stdout.String()) assert.Empty(t, stderr.String(), "version output must not write to stderr") } @@ -46,7 +46,7 @@ func TestVersionFlagDefaultsToDevMetadata(t *testing.T) { root.SetArgs([]string{"--version"}) require.NoError(t, root.ExecuteContext(context.Background())) - assert.Equal(t, "template-mcp-codemode dev (none) built unknown\n", stdout.String()) + assert.Equal(t, "agentcompute dev (none) built unknown\n", stdout.String()) } func TestRootCommandRegistersTransportSubcommands(t *testing.T) { diff --git a/internal/mcpserver/server.go b/internal/mcpserver/server.go index 4415f60..05d2e7e 100644 --- a/internal/mcpserver/server.go +++ b/internal/mcpserver/server.go @@ -1,4 +1,4 @@ -// Package mcpserver builds the transport-agnostic MCP server for this template. +// Package mcpserver builds the transport-agnostic MCP server for this repository. // // The server defined here knows nothing about how it is connected to a client: // the same *mcp.Server is driven by the stdio and http subcommands in @@ -17,7 +17,7 @@ import ( "github.com/meigma/codemode" hostmcp "github.com/meigma/codemode/mcpserver" - "github.com/meigma/template-mcp-codemode/internal/templateinfo" + "github.com/GilmanLab/agentcompute/internal/templateinfo" ) // Dependencies holds the shared collaborators a real server's capabilities diff --git a/internal/templateinfo/info.go b/internal/templateinfo/info.go index 9a89196..5d42587 100644 --- a/internal/templateinfo/info.go +++ b/internal/templateinfo/info.go @@ -2,8 +2,7 @@ // // It is the single source of truth for the application name and title: the CLI // command name, the MCP server implementation name reported to clients, and the -// environment-variable prefix all derive from [Name]. Renaming this template to -// your own project starts here. +// environment-variable prefix all derive from [Name]. package templateinfo import "strings" @@ -12,13 +11,13 @@ const ( // Name is the application and binary name. It is used as the root command // name, the MCP server implementation name, and the base of the // environment-variable prefix (see [EnvPrefix]). - Name = "template-mcp-codemode" + Name = "agentcompute" // Title is the human-readable server title shown to MCP clients. - Title = "Meigma CodeMode MCP server template" + Title = "agentcompute CodeMode MCP server" ) // EnvPrefix returns the prefix for the application's environment variables, -// for example TEMPLATE_MCP_CODEMODE_ADDR. It is derived from [Name] so a +// for example AGENTCOMPUTE_ADDR. It is derived from [Name] so a // rename keeps the command name and the environment variables in sync. func EnvPrefix() string { return strings.ToUpper(strings.ReplaceAll(Name, "-", "_")) diff --git a/internal/templateinfo/info_test.go b/internal/templateinfo/info_test.go index 40a6d63..7a28204 100644 --- a/internal/templateinfo/info_test.go +++ b/internal/templateinfo/info_test.go @@ -9,5 +9,5 @@ import ( func TestEnvPrefix(t *testing.T) { t.Parallel() - assert.Equal(t, "TEMPLATE_MCP_CODEMODE", EnvPrefix()) + assert.Equal(t, "AGENTCOMPUTE", EnvPrefix()) } diff --git a/melange.yaml b/melange.yaml index 2d0219a..3aa0191 100644 --- a/melange.yaml +++ b/melange.yaml @@ -1,4 +1,4 @@ -# Builds the template-mcp-codemode binary into a signed Wolfi apk; apko.yaml +# Builds the agentcompute binary into a signed Wolfi apk; apko.yaml # assembles it into the minimal multi-arch runtime image. version/commit/date are # stamped into the binary via -ldflags, mirroring GoReleaser and the former Dockerfile. # @@ -6,10 +6,10 @@ # The build-time version/commit/date are injected via --vars-file (see the release # workflow and the `image-local` mise task), so no date math runs inside the sandbox. package: - name: template-mcp-codemode + name: agentcompute version: "0.0.0" # x-release-please-version epoch: 0 - description: Meigma CodeMode MCP server template + description: agentcompute CodeMode MCP server environment: contents: @@ -32,8 +32,8 @@ pipeline: # go/build auto-adds -trimpath and installs to /usr/bin/. - uses: go/build with: - packages: ./cmd/template-mcp-codemode - output: template-mcp-codemode + packages: ./cmd/agentcompute + output: agentcompute go-package: go-1.26 modroot: . strip: "-s -w" diff --git a/mise.toml b/mise.toml index e898f25..35e018e 100644 --- a/mise.toml +++ b/mise.toml @@ -1,4 +1,4 @@ -# mise: tool + environment management for template-mcp-codemode. +# mise: tool + environment management for agentcompute. # # mise REPLACES Proto (the former .prototools + .moon/proto/*). The committed # mise.lock records per-platform download URLs + checksums; for aqua-backed tools @@ -49,7 +49,7 @@ GOTOOLCHAIN = "local" # Linux, so `--runner docker` makes `image-local` work on macOS too (Docker Desktop # runs the Linux build container). [tasks.image-local] -description = "Build the apko image for the host arch and load it into Docker as template-mcp-codemode:dev" +description = "Build the apko image for the host arch and load it into Docker as agentcompute:dev" run = ''' set -euo pipefail arch="$(go env GOARCH)" @@ -60,10 +60,10 @@ printf 'version: "dev"\ncommit: "%s"\ndate: "%s"\n' \ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .melange-vars.local.yaml melange build melange.yaml --arch "$arch" --signing-key melange.rsa --runner docker \ --source-dir . --vars-file .melange-vars.local.yaml -apko build apko.yaml template-mcp-codemode:dev image.tar --arch "$arch" --keyring-append ./melange.rsa.pub +apko build apko.yaml agentcompute:dev image.tar --arch "$arch" --keyring-append ./melange.rsa.pub docker load < image.tar -docker tag "template-mcp-codemode:dev-$arch" template-mcp-codemode:dev -echo "loaded template-mcp-codemode:dev (host arch $arch)" +docker tag "agentcompute:dev-$arch" agentcompute:dev +echo "loaded agentcompute:dev (host arch $arch)" ''' [settings] diff --git a/moon.yml b/moon.yml index 7e218e8..d0b6ee4 100644 --- a/moon.yml +++ b/moon.yml @@ -3,11 +3,11 @@ layer: 'application' stack: 'backend' project: - title: 'template-mcp-codemode' - description: 'Meigma CodeMode MCP server template.' - owner: 'meigma' + title: 'agentcompute' + description: 'agentcompute CodeMode MCP server.' + owner: 'GilmanLab' maintainers: - - 'meigma' + - 'GilmanLab' # Every task runs against `system` binaries that mise (mise.toml + mise.lock) puts # on PATH; moon manages no language toolchain itself. @@ -71,11 +71,11 @@ tasks: mutex: 'golangci-lint' build: - command: 'go build -o bin/template-mcp-codemode ./cmd/template-mcp-codemode' + command: 'go build -o bin/agentcompute ./cmd/agentcompute' inputs: - '@group(goSources)' outputs: - - 'bin/template-mcp-codemode' + - 'bin/agentcompute' test: # Space-separated -coverprofile (not `=coverage.out`): under the `system` @@ -93,7 +93,7 @@ tasks: # against the released binary and the published container image, so a break in # the end-to-end path surfaces on every PR instead of at release time. smoke: - command: 'uv run .github/scripts/mcp_smoke.py -- bin/template-mcp-codemode stdio' + command: 'uv run .github/scripts/mcp_smoke.py -- bin/agentcompute stdio' deps: - 'root:build' inputs: diff --git a/release-please-config.json b/release-please-config.json index f99e73a..d1de4c5 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -9,7 +9,7 @@ "bump-patch-for-minor-pre-major": true, "packages": { ".": { - "package-name": "template-mcp-codemode", + "package-name": "agentcompute", "initial-version": "0.1.0", "changelog-path": "CHANGELOG.md", "extra-files": ["melange.yaml", "apko.yaml"] diff --git a/spikes/images/README.md b/spikes/images/README.md new file mode 100644 index 0000000..c6c9f8b --- /dev/null +++ b/spikes/images/README.md @@ -0,0 +1,38 @@ +# spikes/images + +Throwaway publisher and importer for the `router` image. It exists to prove +the path GHCR release → digest → verified bytes → `incus image import` → boot +test → alias promotion before that logic moves into the Go server's catalog +reconciler (Phase 2). It is deliberately small and ugly; do not extend it. + +It is a separate Go module so the server module never depends on it. Pinned: +`github.com/imgoci/go v0.1.0` (pre-v1; bump deliberately). + +## Commands + +```sh +cd spikes/images && go build -o images . + +# Publish an immutable release (refuses an existing tag). +GHCR_USERNAME= GHCR_TOKEN= \ + ./images publish --ref ghcr.io/gilmanlab/agentcompute/router: \ + --version --file router.tar.xz + +# Fetch and verify by digest only. +./images fetch --ref ghcr.io/gilmanlab/agentcompute/router@sha256: \ + --output router.tar.xz + +# Verify, import, boot-test, then move the `router` alias. +INCUS_CONF= \ + ./images import --ref ghcr.io/gilmanlab/agentcompute/router@sha256: \ + --remote nas01 --project image-build +``` + +`import` never touches the existing `router` alias until the candidate +fingerprint has booted and run `nft --version`, `vtysh --help`, `tc -V`, +`dnsmasq --version`, `wg --version`, and `tcpdump --version`. The test +instance is deleted on every exit path. `GHCR_TOKEN` is only presented to +`ghcr.io`; anything else is rejected before a connection is made. + +`smoke.sh` is the CI variant: it boots a local, unpublished tarball under a +temporary alias in the same project and cleans up only what it created. diff --git a/spikes/images/go.mod b/spikes/images/go.mod new file mode 100644 index 0000000..ebbb3e9 --- /dev/null +++ b/spikes/images/go.mod @@ -0,0 +1,18 @@ +module github.com/GilmanLab/agentcompute/spikes/images + +go 1.26.6 + +require github.com/imgoci/go v0.1.0 + +require ( + github.com/distribution/reference v0.6.0 // indirect + github.com/gowebpki/jcs v1.0.1 // indirect + github.com/imgoci/bigoci v0.2.0 // indirect + github.com/imgoci/go-oci-blob v1.1.1 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/ulikunitz/xz v0.5.16 // indirect + golang.org/x/sync v0.22.0 // indirect + oras.land/oras-go/v2 v2.6.2 // indirect +) diff --git a/spikes/images/go.sum b/spikes/images/go.sum new file mode 100644 index 0000000..67ed695 --- /dev/null +++ b/spikes/images/go.sum @@ -0,0 +1,125 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Shopify/toxiproxy/v2 v2.12.0 h1:d1x++lYZg/zijXPPcv7PH0MvHMzEI5aX/YuUi/Sw+yg= +github.com/Shopify/toxiproxy/v2 v2.12.0/go.mod h1:R9Z38Pw6k2cGZWXHe7tbxjGW9azmY1KbDQJ1kd+h7Tk= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= +github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU= +github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= +github.com/imgoci/bigoci v0.2.0 h1:3AN5MvnyskqUYJf+YLFQdEMa7lTR9B99HGTNauEkUEE= +github.com/imgoci/bigoci v0.2.0/go.mod h1:D0pJ+crQm9wrwbe4jyNom5N5Z59m8RgM2rpF12awU9c= +github.com/imgoci/go v0.1.0 h1:rhxNeVA+P45wDrTiOfGC1bD0QrbO/ZnFmOjgaJDnNZw= +github.com/imgoci/go v0.1.0/go.mod h1:5HLzzbZRztb9rm5KSbXkm8fLKXcL0DnUGxSlcjWQMPU= +github.com/imgoci/go-oci-blob v1.1.1 h1:30u/KJh62XmTh/XMf0q0vSi6VvYTxF4K97+V1TmCwio= +github.com/imgoci/go-oci-blob v1.1.1/go.mod h1:yWpqCp3utGnLBvbmoAyU9jzsU+hpNVY1OYgjC0f2s6w= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= +github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= +github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= +github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs= +github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI= +github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= +github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0= +github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo= +oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4= diff --git a/spikes/images/main.go b/spikes/images/main.go new file mode 100644 index 0000000..0d63a39 --- /dev/null +++ b/spikes/images/main.go @@ -0,0 +1,258 @@ +// Command images is a temporary router publisher and Incus importer. +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "os" + "os/exec" + "os/signal" + "path/filepath" + "regexp" + "strings" + "syscall" + "time" + + imgoci "github.com/imgoci/go" +) + +const representation = "x-gilmanlab-incus-container" +const role = "x-gilmanlab-unified" + +var digestRef = regexp.MustCompile(`^ghcr\.io/[a-z0-9._/-]+@sha256:[0-9a-f]{64}$`) +var incusName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) + +var commands = [][]string{ + {"nft", "--version"}, + {"vtysh", "--help"}, + {"tc", "-V"}, + {"dnsmasq", "--version"}, + {"wg", "--version"}, + {"tcpdump", "--version"}, +} + +func main() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + if err := run(ctx, os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, "images:", err) + os.Exit(1) + } +} + +func run(ctx context.Context, args []string) error { + if len(args) == 0 { + return errors.New("usage: images publish|fetch|import [flags]") + } + mode := args[0] + if mode != "publish" && mode != "fetch" && mode != "import" { + return fmt.Errorf("unknown command %q", mode) + } + flags := flag.NewFlagSet(mode, flag.ContinueOnError) + ref := flags.String("ref", "", "GHCR reference (digest required for fetch/import)") + file := flags.String("file", "", "unified router tarball to publish") + version := flags.String("version", "", "immutable release version to publish") + output := flags.String("output", "", "destination tarball for fetch") + remote := flags.String("remote", "", "configured Incus remote name (required for import)") + project := flags.String("project", "", "Incus project name (required for import)") + if err := flags.Parse(args[1:]); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } + return err + } + if flags.NArg() != 0 { + return errors.New("unexpected positional arguments") + } + if !strings.HasPrefix(*ref, "ghcr.io/") { + return errors.New("--ref must name ghcr.io; credentials are scoped to this registry") + } + var options []imgoci.Option + if token := os.Getenv("GHCR_TOKEN"); token != "" { + user := os.Getenv("GHCR_USERNAME") + if user == "" { + return errors.New("GHCR_USERNAME is required with GHCR_TOKEN") + } + options = append(options, imgoci.WithCredentials(user, token)) + } + client, err := imgoci.New(options...) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(ctx, 15*time.Minute) + defer cancel() + if mode == "publish" { + if *file == "" || *version == "" { + return errors.New("publish requires --file and --version") + } + // Immutable means this tool refuses to replace an existing reference. + if _, err := client.Fetch(ctx, imgoci.Reference(*ref)); err == nil { + return fmt.Errorf("release already exists: %s", *ref) + } else if !errors.Is(err, imgoci.ErrNotFound) { + return fmt.Errorf("checking release tag: %w", err) + } + digest, err := client.Publish(ctx, imgoci.Reference(*ref), imgoci.ReleaseSpec{ + Name: "router", + Version: *version, + Annotations: map[string]string{ + "org.opencontainers.image.source": "https://github.com/GilmanLab/agentcompute", + }, + Files: []imgoci.FileSpec{ + {Source: imgoci.FromFile(*file), Filename: "router.tar.xz", Selector: imgoci.Selector{ + Architecture: "amd64", + Target: "incus", + Representation: representation, + Role: role, + Compression: "none", + }}, + }, + }) + if err != nil { + return err + } + return json.NewEncoder(os.Stdout).Encode(map[string]string{"reference": *ref, "digest": digest.String()}) + } + if !digestRef.MatchString(*ref) { + return errors.New("fetch/import requires --ref ghcr.io/owner/image@sha256:<64 lowercase hex digits>") + } + if mode == "import" && + (!incusName.MatchString(*remote) || !incusName.MatchString(*project) || *project == "default") { + return errors.New("import requires named --remote and non-default --project") + } + if mode == "fetch" && *output == "" { + return errors.New("fetch requires --output") + } + release, err := client.Fetch(ctx, imgoci.Reference(*ref)) + if err != nil { + return err + } + selected, err := client.Resolve(release, imgoci.ResolveQuery{ + Architecture: "amd64", + Target: "incus", + Representation: representation, + Roles: []string{role}, + Compressions: []string{"none"}, + }) + if err != nil { + return err + } + entries := selected.Entries() + if len(entries) != 1 { + return errors.New("router release must resolve exactly one unified archive") + } + temporary, err := os.MkdirTemp("", "agentcompute-image-") + if err != nil { + return err + } + defer os.RemoveAll(temporary) + destination := filepath.Join(temporary, "router.tar.xz") + if mode == "fetch" { + destination = *output + } + if err := client.FetchFiles( + ctx, + release, + selected, + imgoci.ToFiles(map[string]string{role: destination}), + ); err != nil { + return err + } + fingerprint := strings.TrimPrefix(entries[0].ContentDigest.String(), "sha256:") + if mode == "fetch" { + return json.NewEncoder(os.Stdout). + Encode(map[string]any{"digest": release.Digest(), "fingerprint": fingerprint, "bytes": entries[0].ContentSize, "file": destination}) + } + incus := func(ctx context.Context, args ...string) error { + command := exec.CommandContext(ctx, "incus", append([]string{"--quiet", "--project", *project}, args...)...) + command.Stdout, command.Stderr = os.Stderr, os.Stderr + if err := command.Run(); err != nil { + return fmt.Errorf("incus %s: %w", strings.Join(args, " "), err) + } + return nil + } + query := func(path string, result any) error { + command := exec.CommandContext(ctx, "incus", "query", *remote+":"+path+"?project="+*project+"&recursion=1") + command.Stderr = os.Stderr + data, err := command.Output() + if err != nil { + return fmt.Errorf("querying Incus %s: %w", path, err) + } + return json.Unmarshal(data, result) + } + // Content is fully verified before Incus sees it; the existing router alias + // remains untouched until the candidate fingerprint passes its boot test. + var images []struct{ Fingerprint string } + if err := query("/1.0/images", &images); err != nil { + return err + } + present := false + for _, image := range images { + present = present || image.Fingerprint == fingerprint + } + if !present { + if err := incus(ctx, "image", "import", destination, *remote+":"); err != nil { + return err + } + } + instance := "router-check-" + filepath.Base(temporary) + instance = strings.ReplaceAll(instance, "agentcompute-image-", "") + if err := incus(ctx, "init", *remote+":"+fingerprint, *remote+":"+instance); err != nil { + return err + } + removed := false + defer func() { + if removed { + return + } + cleanup, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + if err := incus(cleanup, "delete", "-f", *remote+":"+instance); err != nil { + fmt.Fprintln(os.Stderr, "cleanup:", err) + } + }() + if err := incus(ctx, "start", *remote+":"+instance); err != nil { + return err + } + for _, command := range commands { + args := append([]string{"exec", *remote + ":" + instance, "--"}, command...) + if err := incus(ctx, args...); err != nil { + return err + } + } + if err := incus(ctx, "delete", "-f", *remote+":"+instance); err != nil { + return err + } + removed = true + var aliases []struct{ Name, Target string } + if err := query("/1.0/images/aliases", &aliases); err != nil { + return err + } + aliasExists := false + for _, alias := range aliases { + aliasExists = aliasExists || alias.Name == "router" + } + if aliasExists { + payload, err := json.Marshal(map[string]string{"target": fingerprint}) + if err != nil { + return err + } + if err := incus( + ctx, + "query", + "-X", + "PUT", + "-d", + string(payload), + *remote+":/1.0/images/aliases/router?project="+*project, + ); err != nil { + return err + } + } else if err := incus(ctx, "image", "alias", "create", *remote+":router", fingerprint); err != nil { + return err + } + return json.NewEncoder(os.Stdout). + Encode(map[string]any{"digest": release.Digest(), "fingerprint": fingerprint, "bytes": entries[0].ContentSize, "remote": *remote, "project": *project, "alias": "router"}) +} diff --git a/spikes/images/smoke.sh b/spikes/images/smoke.sh new file mode 100755 index 0000000..9c4beaa --- /dev/null +++ b/spikes/images/smoke.sh @@ -0,0 +1,257 @@ +#!/usr/bin/env bash +# Boot smoke test for a router image that has NOT been published yet. +# +# `spikes/images` (main.go) covers the published path: fetch a GHCR release by +# digest, verify it, import it, boot it, then promote the shared `router` alias. +# This script covers the gap that tool deliberately leaves open — proving a +# freshly built router.tar.xz actually boots and carries its router tooling +# BEFORE anything is pushed to a registry. Consequently it: +# +# * never promotes (or touches) the shared `router` alias, and asserts that +# the alias target is unchanged when it is done; +# * imports the candidate image only when the Incus server does not already +# have that fingerprint, and deletes ONLY what this run created; +# * fails the run when cleanup fails, so leaked instances/images are loud. +# +# Usage: +# spikes/images/smoke.sh --file --remote --project +# [--suffix ] [--log ] [--timeout ] +# +# Requirements: the `incus` client on PATH (CI installs the SHA-pinned binary, +# see .github/workflows/images-publish.yml), python3 (stdlib only), sha256sum, +# and INCUS_CONF/remote configuration that can reach the server. +# +# stdout is a single JSON object describing the run; progress and guest command +# output go to stderr and to the log file. + +set -euo pipefail + +die() { + printf 'smoke: %s\n' "$*" >&2 + exit 1 +} + +note() { + printf 'smoke: %s\n' "$*" >&2 +} + +file="" +remote="" +project="" +suffix="" +log="" +timeout=180 + +while [ "$#" -gt 0 ]; do + case "$1" in + --file) + file="${2:-}" + shift 2 + ;; + --remote) + remote="${2:-}" + shift 2 + ;; + --project) + project="${2:-}" + shift 2 + ;; + --suffix) + suffix="${2:-}" + shift 2 + ;; + --log) + log="${2:-}" + shift 2 + ;; + --timeout) + timeout="${2:-}" + shift 2 + ;; + -h | --help) + sed -n '2,30p' "$0" + exit 0 + ;; + *) + die "unknown argument: $1" + ;; + esac +done + +[ -n "$file" ] || die "--file is required" +[ -n "$remote" ] || die "--remote is required" +[ -n "$project" ] || die "--project is required" +[ -f "$file" ] || die "image file not found: $file" +[ "$project" != "default" ] || die "--project must not be the default project" +command -v incus >/dev/null 2>&1 || die "incus client not found on PATH" +command -v python3 >/dev/null 2>&1 || die "python3 not found on PATH" +command -v sha256sum >/dev/null 2>&1 || die "sha256sum not found on PATH" +case "$timeout" in +'' | *[!0-9]*) die "--timeout must be a whole number of seconds" ;; +esac + +if [ -z "$suffix" ]; then + suffix="$(date -u +%Y%m%d%H%M%S)-$$" +fi +# Incus instance and alias names: lowercase alphanumerics and dashes only. +suffix="$(printf '%s' "$suffix" | tr '[:upper:]_.' '[:lower:]--' | tr -cd 'a-z0-9-')" +[ -n "$suffix" ] || die "--suffix contains no usable characters" +name="router-smoke-$suffix" +case "$name" in +[a-z0-9]*) ;; +*) die "derived name is not a valid Incus name: $name" ;; +esac +[ "${#name}" -le 63 ] || die "derived name is longer than 63 characters: $name" + +if [ -z "$log" ]; then + log="$(mktemp -t router-smoke-XXXXXX.log)" +fi +: >"$log" + +# The Incus fingerprint of a unified image is the SHA-256 of the tarball itself, +# which is also the content digest `spikes/images` publishes and verifies. +fingerprint="$(sha256sum "$file" | cut -d' ' -f1)" + +incus_q() { + incus --quiet --project "$project" "$@" +} + +api() { + incus query "$remote:$1?project=$project&recursion=1" +} + +has_fingerprint() { + api /1.0/images | python3 -c ' +import json, sys +wanted = sys.argv[1] +images = json.load(sys.stdin) or [] +sys.exit(0 if any(image.get("fingerprint") == wanted for image in images) else 1) +' "$fingerprint" +} + +router_alias_target() { + api /1.0/images/aliases | python3 -c ' +import json, sys +for alias in json.load(sys.stdin) or []: + if alias.get("name") == "router": + print(alias.get("target", "")) + break +else: + print("") +' +} + +image_imported=0 +alias_created=0 +instance_created=0 +router_alias_before="" + +cleanup() { + status=$? + trap - EXIT + failures=0 + + if [ "$instance_created" -eq 1 ]; then + note "deleting instance $name" + if incus_q delete -f "$remote:$name"; then + instance_created=0 + else + note "FAILED to delete instance $name" + failures=1 + fi + fi + + if [ "$alias_created" -eq 1 ]; then + note "deleting temporary alias $name" + if incus_q image alias delete "$remote:$name"; then + alias_created=0 + else + note "FAILED to delete alias $name" + failures=1 + fi + fi + + # Only ever delete the candidate image when THIS run imported it; a + # fingerprint that was already on the server is shared state. + if [ "$image_imported" -eq 1 ]; then + note "deleting image $fingerprint imported by this run" + if incus_q image delete "$remote:$fingerprint"; then + image_imported=0 + else + note "FAILED to delete image $fingerprint" + failures=1 + fi + fi + + if router_alias_after="$(router_alias_target)"; then + if [ "$router_alias_after" != "$router_alias_before" ]; then + note "FAILED invariant: router alias changed from '${router_alias_before:-}' to '${router_alias_after:-}'" + failures=1 + fi + else + note "FAILED to re-read the router alias for the promotion invariant" + failures=1 + fi + + if [ "$failures" -ne 0 ]; then + note "cleanup failed; the project may need manual inspection" + exit 1 + fi + + exit "$status" +} + +note "candidate $file fingerprint $fingerprint" +router_alias_before="$(router_alias_target)" || + die "cannot reach $remote (project $project); check INCUS_CONF and network" +note "router alias currently targets ${router_alias_before:-}" + +trap cleanup EXIT + +if has_fingerprint; then + note "server already has fingerprint $fingerprint; leaving that image untouched" +else + note "importing candidate as $name" + if incus_q image import "$file" "$remote:" --alias "$name"; then + image_imported=1 + alias_created=1 + else + # Another run may have imported the same fingerprint concurrently. + has_fingerprint || die "image import failed for $fingerprint" + note "import lost a race; the fingerprint is present and stays untouched" + fi + has_fingerprint || die "server does not report fingerprint $fingerprint after import" +fi + +note "creating instance $name from $fingerprint" +incus_q init "$remote:$fingerprint" "$remote:$name" +instance_created=1 +incus_q start "$remote:$name" + +deadline=$(($(date +%s) + timeout)) +until incus_q exec "$remote:$name" -- /bin/true >/dev/null 2>&1; do + [ "$(date +%s)" -lt "$deadline" ] || + die "instance $name was not reachable within ${timeout}s" + sleep 2 +done +note "instance $name is up" + +check() { + printf '=== %s\n' "$*" >>"$log" + if ! incus_q exec "$remote:$name" -- "$@" 2>&1 | tee -a "$log" >&2; then + printf '=== FAILED: %s\n' "$*" >>"$log" + die "router tool check failed: $* (log: $log)" + fi +} + +check nft --version +check vtysh --help +check tc -V +check dnsmasq --version +check wg --version +check tcpdump --version +note "all six router tool checks passed" + +printf '{"fingerprint":"%s","file":"%s","remote":"%s","project":"%s","instance":"%s","image_imported":%s,"checks":6,"log":"%s"}\n' \ + "$fingerprint" "$file" "$remote" "$project" "$name" \ + "$([ "$image_imported" -eq 1 ] && printf true || printf false)" "$log" diff --git a/tools/proxy/.mockery.yaml b/tools/proxy/.mockery.yaml index f2c3263..173c1f2 100644 --- a/tools/proxy/.mockery.yaml +++ b/tools/proxy/.mockery.yaml @@ -24,7 +24,7 @@ formatter-options: comments: true format-only: true fragment: false - local-prefix: github.com/meigma/template-mcp-codemode + local-prefix: github.com/GilmanLab/agentcompute tab-indent: true tab-width: 8 generate: true @@ -39,6 +39,6 @@ template-schema: '{{.Template}}.schema.json' packages: # The reloader hexagon's ports: Watcher, Builder, Upstream, ChildSession, # Frontend (plus any future boundary interfaces in the package). - github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader: + github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader: config: all: true diff --git a/tools/proxy/README.md b/tools/proxy/README.md index 2515650..4bd0617 100644 --- a/tools/proxy/README.md +++ b/tools/proxy/README.md @@ -2,9 +2,9 @@ `mcp-devproxy` keeps one client session open while rebuilding and replacing a STDIO MCP server. The client connects to the proxy once; the proxy watches source directories, builds a unique child binary, initializes it, swaps the active child, and forwards calls to the new process. -The proxy lives in the nested module `github.com/meigma/template-mcp-codemode/tools/proxy`, so its development dependencies do not enter the server module or release artifacts. +The proxy lives in the nested module `github.com/GilmanLab/agentcompute/tools/proxy`, so its development dependencies do not enter the server module or release artifacts. -This template is CodeMode-native. Every healthy child exposes the same three outer MCP tools: +This server is CodeMode-native. Every healthy child exposes the same three outer MCP tools: - `search_api` - `describe_api` @@ -37,11 +37,11 @@ Two parts of the wrapper are required: - `>&2` keeps build output away from stdout, which carries JSON-RPC. - `proxy:build` declares its inputs and outputs, so Moon can skip a warm build without leaving a missing or stale proxy binary. -The proxy has defaults for this repository. A bare `mcp-devproxy` builds `./cmd/template-mcp-codemode` and runs the artifact with `stdio`. To provide every value explicitly: +The proxy has defaults for this repository. A bare `mcp-devproxy` builds `./cmd/agentcompute` and runs the artifact with `stdio`. To provide every value explicitly: ```sh mcp-devproxy \ - --build "go build -o {{artifact}} ./cmd/template-mcp-codemode" \ + --build "go build -o {{artifact}} ./cmd/agentcompute" \ --watch cmd --watch internal \ --debounce 300ms \ --quiesce 5s \ @@ -57,7 +57,7 @@ Flags take precedence over `MCP_DEVPROXY_*` environment variables, which take pr | Flag | Environment | Default | Meaning | | --- | --- | --- | --- | -| `--build` | `MCP_DEVPROXY_BUILD` | `go build -o {{artifact}} ./cmd/template-mcp-codemode` | Build command template. It is split on whitespace without a shell and must contain `{{artifact}}`. | +| `--build` | `MCP_DEVPROXY_BUILD` | `go build -o {{artifact}} ./cmd/agentcompute` | Build command template. It is split on whitespace without a shell and must contain `{{artifact}}`. | | `--watch` | `MCP_DEVPROXY_WATCH` | `cmd`, `internal` | Recursively watched directory. Repeat the flag; the environment form is whitespace-separated. | | `--dir` | `MCP_DEVPROXY_DIR` | Current directory | Working directory for the build command. | | `--debounce` | `MCP_DEVPROXY_DEBOUNCE` | `300ms` | Time used to combine a burst of file events into one build. | diff --git a/tools/proxy/cmd/mcp-devproxy/main.go b/tools/proxy/cmd/mcp-devproxy/main.go index 29723da..48a2a99 100644 --- a/tools/proxy/cmd/mcp-devproxy/main.go +++ b/tools/proxy/cmd/mcp-devproxy/main.go @@ -10,7 +10,7 @@ import ( "os/signal" "syscall" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/cli" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/cli" ) func main() { diff --git a/tools/proxy/go.mod b/tools/proxy/go.mod index a54cf02..05973e0 100644 --- a/tools/proxy/go.mod +++ b/tools/proxy/go.mod @@ -1,4 +1,4 @@ -module github.com/meigma/template-mcp-codemode/tools/proxy +module github.com/GilmanLab/agentcompute/tools/proxy go 1.26.6 diff --git a/tools/proxy/internal/build/build.go b/tools/proxy/internal/build/build.go index dd4cd87..3f95bcb 100644 --- a/tools/proxy/internal/build/build.go +++ b/tools/proxy/internal/build/build.go @@ -14,7 +14,7 @@ import ( "sync/atomic" "time" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" ) // artifactToken is the placeholder in the build command template replaced diff --git a/tools/proxy/internal/build/build_test.go b/tools/proxy/internal/build/build_test.go index 61df999..debf286 100644 --- a/tools/proxy/internal/build/build_test.go +++ b/tools/proxy/internal/build/build_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/build" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/build" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" ) // promptReturnBound is the ceiling on how long a cancelled Build may take to diff --git a/tools/proxy/internal/cli/assemble.go b/tools/proxy/internal/cli/assemble.go index 8966eaa..1501e03 100644 --- a/tools/proxy/internal/cli/assemble.go +++ b/tools/proxy/internal/cli/assemble.go @@ -11,11 +11,11 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/build" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/downstream" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/upstream" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/watch" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/build" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/downstream" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/upstream" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/watch" ) // seams carries test-only adapter overrides; the zero value selects the diff --git a/tools/proxy/internal/cli/config.go b/tools/proxy/internal/cli/config.go index 69f4628..48ea6b9 100644 --- a/tools/proxy/internal/cli/config.go +++ b/tools/proxy/internal/cli/config.go @@ -13,7 +13,7 @@ import ( // config carries the resolved proxy configuration from the flag/environment // layer into newProxy. The fields are generic; applyTemplateDefaults fills -// the empty ones with this template's layout before validation. +// the empty ones with this repository's layout before validation. type config struct { // buildCommand is the build command template: whitespace-split, no // shell, with {{artifact}} replaced per cycle by the build adapter. diff --git a/tools/proxy/internal/cli/defaults.go b/tools/proxy/internal/cli/defaults.go index 14c95b5..780d6f5 100644 --- a/tools/proxy/internal/cli/defaults.go +++ b/tools/proxy/internal/cli/defaults.go @@ -1,8 +1,8 @@ package cli -// This file is the zero-config convenience layer for THIS template's layout: -// inside the template repository a bare `mcp-devproxy` builds and serves -// ./cmd/template-mcp-codemode. It is kept apart from the generic flag handling so +// This file is the zero-config convenience layer for this repository's layout: +// a bare `mcp-devproxy` builds and serves +// ./cmd/agentcompute. It is kept apart from the generic flag handling so // extraction to a standalone repository stays clean — delete this file and // its one call in resolveConfig, and nothing else changes. @@ -12,7 +12,7 @@ import ( ) // defaultBuildCommand builds the template server into the cycle's artifact. -const defaultBuildCommand = "go build -o {{artifact}} ./cmd/template-mcp-codemode" +const defaultBuildCommand = "go build -o {{artifact}} ./cmd/agentcompute" // defaultChildTransport is the template server's stdio transport subcommand. const defaultChildTransport = "stdio" @@ -24,7 +24,7 @@ func defaultWatchDirs() []string { return []string{"cmd", "internal"} } func defaultChildArgv() []string { return []string{artifactToken, defaultChildTransport} } // applyTemplateDefaults fills each empty config field independently with -// this template's default — a user may override --build and keep the default +// this repository's default — a user may override --build and keep the default // watch directories — and logs every defaulted value so zero-config behavior // is never silent. func applyTemplateDefaults(cfg *config, logger *slog.Logger) { diff --git a/tools/proxy/internal/cli/integration_test.go b/tools/proxy/internal/cli/integration_test.go index 1fa5944..dfef731 100644 --- a/tools/proxy/internal/cli/integration_test.go +++ b/tools/proxy/internal/cli/integration_test.go @@ -27,8 +27,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/upstream" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/upstream" ) // waitTimeout bounds every asynchronous wait in this suite. diff --git a/tools/proxy/internal/cli/root.go b/tools/proxy/internal/cli/root.go index bf0de53..a939107 100644 --- a/tools/proxy/internal/cli/root.go +++ b/tools/proxy/internal/cli/root.go @@ -3,7 +3,7 @@ // The proxy is a single root command — no transport subcommands: v1 is // stdio-downstream-only, and a future HTTP downstream is a new adapter in // internal/downstream, not a new verb. The generic flags live here; the -// zero-config defaults for this template's layout are deliberately isolated +// zero-config defaults for this repository's layout are deliberately isolated // in defaults.go so extraction to a standalone repository stays clean. package cli @@ -95,9 +95,9 @@ type launchFunc func(cmd *cobra.Command, cfg config, logger *slog.Logger) error // NewRootCommand creates the mcp-devproxy Cobra command. // // The child command is positional argv after "--"; everything before it is -// flags. Inside this template repository every flag has a working default +// flags. Inside this repository every flag has a working default // (see defaults.go), so a bare invocation builds and serves -// ./cmd/template-mcp-codemode. +// ./cmd/agentcompute. func NewRootCommand(options Options) *cobra.Command { return newRootCommand(options, launchProxy) } @@ -128,7 +128,7 @@ func newRootCommand(options Options, launch launchFunc) *cobra.Command { "The child command after \"--\" is re-run for every reload cycle with\n" + "{{artifact}} replaced by that cycle's freshly built binary.", Example: " " + appName + " \\\n" + - " --build \"go build -o {{artifact}} ./cmd/template-mcp-codemode\" \\\n" + + " --build \"go build -o {{artifact}} ./cmd/agentcompute\" \\\n" + " --watch cmd --watch internal \\\n" + " -- {{artifact}} stdio", Version: options.Build.Version, diff --git a/tools/proxy/internal/cli/root_test.go b/tools/proxy/internal/cli/root_test.go index b13e840..9ae49cc 100644 --- a/tools/proxy/internal/cli/root_test.go +++ b/tools/proxy/internal/cli/root_test.go @@ -52,7 +52,7 @@ func discardLogger() *slog.Logger { return slog.New(slog.DiscardHandler) } // anything: construction touches no watch paths and spawns no processes. func validProxyConfig() config { return config{ - buildCommand: "go build -o {{artifact}} ./cmd/template-mcp-codemode", + buildCommand: "go build -o {{artifact}} ./cmd/agentcompute", watchDirs: []string{"."}, childArgv: []string{"{{artifact}}", "stdio"}, } @@ -289,7 +289,7 @@ func TestNewProxyConstruction(t *testing.T) { }, { name: "build command without the artifact token surfaces with the flag name", - mutate: func(cfg *config) { cfg.buildCommand = "go build ./cmd/template-mcp-codemode" }, + mutate: func(cfg *config) { cfg.buildCommand = "go build ./cmd/agentcompute" }, wantErr: "--" + buildFlag + ": the build command must reference " + artifactToken, }, { diff --git a/tools/proxy/internal/downstream/downstream.go b/tools/proxy/internal/downstream/downstream.go index 4bc3f18..6de4558 100644 --- a/tools/proxy/internal/downstream/downstream.go +++ b/tools/proxy/internal/downstream/downstream.go @@ -8,7 +8,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" ) // serverName identifies the proxy on the downstream hop when Options.Impl is diff --git a/tools/proxy/internal/downstream/downstream_test.go b/tools/proxy/internal/downstream/downstream_test.go index 8802bc5..bb4a505 100644 --- a/tools/proxy/internal/downstream/downstream_test.go +++ b/tools/proxy/internal/downstream/downstream_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" ) // waitTimeout bounds every asynchronous wait in this suite. diff --git a/tools/proxy/internal/upstream/child.go b/tools/proxy/internal/upstream/child.go index b0340a4..33160e8 100644 --- a/tools/proxy/internal/upstream/child.go +++ b/tools/proxy/internal/upstream/child.go @@ -8,7 +8,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" ) // childSession is one live child MCP connection, implementing the diff --git a/tools/proxy/internal/upstream/upstream.go b/tools/proxy/internal/upstream/upstream.go index 77e9c4c..2e9d204 100644 --- a/tools/proxy/internal/upstream/upstream.go +++ b/tools/proxy/internal/upstream/upstream.go @@ -14,7 +14,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/jsonrpc" "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" ) // artifactToken is the placeholder in the child argv template replaced with diff --git a/tools/proxy/internal/upstream/upstream_test.go b/tools/proxy/internal/upstream/upstream_test.go index bd411fa..4846197 100644 --- a/tools/proxy/internal/upstream/upstream_test.go +++ b/tools/proxy/internal/upstream/upstream_test.go @@ -21,8 +21,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/upstream" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/upstream" ) // waitTimeout bounds every asynchronous wait in this suite. diff --git a/tools/proxy/internal/watch/watch.go b/tools/proxy/internal/watch/watch.go index 90e6dd9..49e7d79 100644 --- a/tools/proxy/internal/watch/watch.go +++ b/tools/proxy/internal/watch/watch.go @@ -11,7 +11,7 @@ import ( "github.com/fsnotify/fsnotify" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" ) // eventBufferSize is the fsnotify event buffer size; it absorbs editor save diff --git a/tools/proxy/internal/watch/watch_test.go b/tools/proxy/internal/watch/watch_test.go index 69c3d04..13e0bb9 100644 --- a/tools/proxy/internal/watch/watch_test.go +++ b/tools/proxy/internal/watch/watch_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/reloader" - "github.com/meigma/template-mcp-codemode/tools/proxy/internal/watch" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/reloader" + "github.com/GilmanLab/agentcompute/tools/proxy/internal/watch" ) const eventTimeout = 5 * time.Second diff --git a/tools/proxy/moon.yml b/tools/proxy/moon.yml index f876308..0ba53f9 100644 --- a/tools/proxy/moon.yml +++ b/tools/proxy/moon.yml @@ -5,9 +5,9 @@ stack: 'backend' project: title: 'mcp-devproxy' description: 'MCP dev proxy that hot-reloads an in-development server behind a stable client session.' - owner: 'meigma' + owner: 'GilmanLab' maintainers: - - 'meigma' + - 'GilmanLab' # Every task runs against `system` binaries that mise (mise.toml + mise.lock) puts # on PATH; moon manages no language toolchain itself. @@ -35,8 +35,8 @@ workspace: # format/lint reuse the root .golangci.yml. No local-prefix override is needed: # goimports matches local-prefixes by string prefix, so the root's -# 'github.com/meigma/template-mcp-codemode' already covers this nested module's path -# ('github.com/meigma/template-mcp-codemode/tools/proxy'). +# 'github.com/GilmanLab/agentcompute' already covers this nested module's path +# ('github.com/GilmanLab/agentcompute/tools/proxy'). tasks: format: command: 'golangci-lint fmt --config ../../.golangci.yml --diff' @@ -91,7 +91,7 @@ tasks: # (`settings.locked`). The mocks are generated in-package (filename # mocks_test.go, pkgname = source package), so there is no separate-package # drift-check task — a stale or broken mock surfaces as a compile/test failure - # in `proxy:test`, matching this template's whitebox-test design. + # in `proxy:test`, matching this repository's whitebox-test design. mockery: command: 'mockery' inputs: