From 4f72f7b8e87b31f3b9b02f9f244b3e4b8f014fdc Mon Sep 17 00:00:00 2001 From: O6lvl4 Date: Sun, 4 Oct 2026 17:00:48 +0900 Subject: [PATCH 1/2] Install the pinned Almide release binary instead of building from source Pin the official v0.66.0 release and commit its archive checksums. The installer downloads the platform archive, refuses it unless the sha256 matches, and installs almide and almide-verify. This replaces a full cargo build of the compiler (about 7.5 minutes locally, 5m46s of a 7m18s image build on a 4-core VPS) with a download of a few seconds. The release binary needs glibc 2.39+, so the Dockerfile moves to Debian trixie. Native almide build still emits Rust and runs cargo, so the build stage keeps the pinned Rust image. Co-Authored-By: Claude Opus 5.5 --- .almide-checksums.sha256 | 4 ++ .almide-release | 1 + .almide-revision | 1 - .github/workflows/test.yml | 2 +- Dockerfile | 10 ++-- scripts/build.sh | 4 +- scripts/install-almide.sh | 59 ++++++++++++----------- tests/installer.test.mjs | 95 ++++++++++++++++++++------------------ 8 files changed, 96 insertions(+), 80 deletions(-) create mode 100644 .almide-checksums.sha256 create mode 100644 .almide-release delete mode 100644 .almide-revision diff --git a/.almide-checksums.sha256 b/.almide-checksums.sha256 new file mode 100644 index 0000000..9de0dd3 --- /dev/null +++ b/.almide-checksums.sha256 @@ -0,0 +1,4 @@ +4c5ba89fdeabea7a07679795a714a5f840e1b4024205b1012554c580bb4820fe almide-linux-aarch64.tar.gz +9a723b256e85b48e36c1a1d6faab9a2d4154e71ea8c0f41b8d9534e2aea6ae50 almide-linux-x86_64.tar.gz +0432fe433bc56e3ce14dab7b5a2c6e4d5de9bb5c58cdb191da77b17181199921 almide-macos-aarch64.tar.gz +5cb07a8115410705249b2b5f46f73561aaa28a4356817e2b8815edbf7c9cace9 almide-macos-x86_64.tar.gz diff --git a/.almide-release b/.almide-release new file mode 100644 index 0000000..a2a9338 --- /dev/null +++ b/.almide-release @@ -0,0 +1 @@ +v0.66.0 diff --git a/.almide-revision b/.almide-revision deleted file mode 100644 index 1ef46ef..0000000 --- a/.almide-revision +++ /dev/null @@ -1 +0,0 @@ -852b028a5706801fd008a753bcbdf8b3ea93156f diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f24f45..6afac24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: cache: npm - name: Install pinned Rust run: rustup toolchain install 1.99.0 --profile minimal - - name: Build pinned Almide + - name: Install pinned Almide release run: ./scripts/install-almide.sh - run: npm ci - run: npm run build diff --git a/Dockerfile b/Dockerfile index d54a620..4db85e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,15 @@ -# Exact Rust version and Almide commit; base-image digests are not locked yet. -FROM rust:1.99.0-bookworm AS build +# Exact Rust version and Almide release (checksum-verified); base-image digests are +# not locked yet. The release binary needs glibc 2.39+, hence Debian trixie. Native +# `almide build` emits Rust and compiles it with cargo, so the build stage keeps Rust. +FROM rust:1.99.0-trixie AS build WORKDIR /work -COPY .almide-revision rust-toolchain.toml ./ +COPY .almide-release .almide-checksums.sha256 rust-toolchain.toml ./ COPY scripts/install-almide.sh scripts/install-almide.sh RUN ./scripts/install-almide.sh COPY src/ src/ RUN mkdir build && .tools/bin/almide build src/native.almd -o build/server -FROM debian:bookworm-slim AS runtime +FROM debian:trixie-slim AS runtime WORKDIR /app COPY --from=build /work/build/server ./server COPY LICENSE /app/LICENSE diff --git a/scripts/build.sh b/scripts/build.sh index 0b3a9f1..494ccde 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -6,8 +6,8 @@ if [[ ! -x "$almide" ]]; then echo 'Compiler missing. Run ./scripts/install-almide.sh first.' >&2; exit 1 fi if [[ -z "${ALMIDE_BIN:-}" ]]; then - cmp -s .almide-revision .tools/bin/almide.revision || { - echo 'Compiler revision mismatch. Re-run ./scripts/install-almide.sh.' >&2; exit 1 + cmp -s .almide-release .tools/bin/almide.release || { + echo 'Compiler release mismatch. Re-run ./scripts/install-almide.sh.' >&2; exit 1 } fi mkdir -p build diff --git a/scripts/install-almide.sh b/scripts/install-almide.sh index 04d5b47..d65e5ff 100755 --- a/scripts/install-almide.sh +++ b/scripts/install-almide.sh @@ -1,34 +1,39 @@ #!/usr/bin/env bash -# Build the exact reviewed compiler revision. Rust 1.99.0, git, and a C toolchain required. +# Install the pinned Almide release binary. The tag is in .almide-release and the +# expected sha256 of each platform archive in .almide-checksums.sha256; an archive +# that does not match is never unpacked or installed. Needs curl and tar. set -euo pipefail cd "$(dirname "$0")/.." -revision=$(tr -d '\r\n' < .almide-revision) -[[ "$revision" =~ ^[0-9a-f]{40}$ ]] || { echo 'Invalid compiler revision' >&2; exit 1; } -source_dir="$PWD/.tools/almide-source" -if [[ ! -d "$source_dir/.git" ]]; then - mkdir -p .tools - git init "$source_dir" - git -C "$source_dir" remote add origin https://github.com/almide/almide.git +tag=$(tr -d '\r\n' < .almide-release) +[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'Invalid release tag in .almide-release' >&2; exit 1; } +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) platform=linux-x86_64 ;; + Linux-aarch64 | Linux-arm64) platform=linux-aarch64 ;; + Darwin-arm64) platform=macos-aarch64 ;; + Darwin-x86_64) platform=macos-x86_64 ;; + *) echo "No Almide release archive for $(uname -s) $(uname -m)" >&2; exit 1 ;; +esac +archive="almide-$platform.tar.gz" +expected=$(awk -v f="$archive" '$2 == f { print $1 }' .almide-checksums.sha256) +[[ "$expected" =~ ^[0-9a-f]{64}$ ]] || { echo "No checksum for $archive" >&2; exit 1; } + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +curl --fail --location --silent --show-error --retry 3 \ + -o "$work/$archive" "https://github.com/almide/almide/releases/download/$tag/$archive" +if command -v sha256sum >/dev/null; then + actual=$(sha256sum "$work/$archive" | awk '{ print $1 }') +else + actual=$(shasum -a 256 "$work/$archive" | awk '{ print $1 }') fi -if [[ "$(git -C "$source_dir" remote get-url origin)" != https://github.com/almide/almide.git ]]; then - echo 'Unexpected compiler source remote' >&2; exit 1 +if [[ "$actual" != "$expected" ]]; then + echo "Checksum mismatch for $archive: expected $expected, got $actual" >&2; exit 1 fi -require_clean_source() { - if [[ -n "$(git -C "$source_dir" status --porcelain --untracked-files=all)" ]]; then - echo 'Compiler source has modified or untracked files. Preserve your edits and use a clean checkout.' >&2 - exit 1 - fi -} -# Never replace local edits, or label them as the pinned upstream revision. -# Normal ignored build artifacts (for example target/) do not make it dirty. -require_clean_source -git -C "$source_dir" fetch --depth 1 origin "$revision" -git -C "$source_dir" checkout --detach "$revision" -[[ "$(git -C "$source_dir" rev-parse HEAD)" == "$revision" ]] -require_clean_source -# The root rust-toolchain.toml selects the pinned Rust toolchain. -cargo build --locked --release --bin almide --manifest-path "$source_dir/Cargo.toml" +tar -xzf "$work/$archive" -C "$work" +# almide verify execs almide-verify from next to itself, so both are installed. mkdir -p .tools/bin -cp "$source_dir/target/release/almide" .tools/bin/almide -printf '%s\n' "$revision" > .tools/bin/almide.revision +for tool in almide almide-verify; do + install -m 0755 "$work/almide-$platform/$tool" ".tools/bin/$tool" +done +printf '%s\n' "$tag" > .tools/bin/almide.release .tools/bin/almide --version diff --git a/tests/installer.test.mjs b/tests/installer.test.mjs index 8a946dd..622465b 100644 --- a/tests/installer.test.mjs +++ b/tests/installer.test.mjs @@ -1,67 +1,72 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; -import { mkdtemp, mkdir, readFile, writeFile, copyFile, rm } from 'node:fs/promises'; +import { mkdtemp, mkdir, readFile, writeFile, copyFile, rm, access } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; -// Real git status/checkout, but no network or compiler build. Only those two -// expensive operations are replaced, so the provenance guard is tested directly. -async function fixture(t) { +// Real checksum, tar and install steps; only the download is replaced by a stub +// curl that serves a locally built archive, so the integrity guard is tested directly. +function platform() { + const os = spawnSync('uname', ['-s'], { encoding: 'utf8' }).stdout.trim(); + const arch = spawnSync('uname', ['-m'], { encoding: 'utf8' }).stdout.trim(); + return { 'Linux-x86_64': 'linux-x86_64', 'Linux-aarch64': 'linux-aarch64', 'Linux-arm64': 'linux-aarch64', + 'Darwin-arm64': 'macos-aarch64', 'Darwin-x86_64': 'macos-x86_64' }[`${os}-${arch}`]; +} + +async function fixture(t, { tag = 'v9.9.9', tamper = false } = {}) { const root = await mkdtemp(join(tmpdir(), 'almide-installer-')); t.after(() => rm(root, { recursive: true, force: true })); - const source = join(root, '.tools/almide-source'); + const name = `almide-${platform()}`; + const staging = join(root, 'staging', name); const bin = join(root, 'test-bin'); - await mkdir(source, { recursive: true }); + await mkdir(staging, { recursive: true }); await mkdir(bin); await mkdir(join(root, 'scripts')); await copyFile(new URL('../scripts/install-almide.sh', import.meta.url), join(root, 'scripts/install-almide.sh')); - const runGit = (...args) => { - const result = spawnSync('git', ['-C', source, ...args], { encoding: 'utf8' }); - assert.equal(result.status, 0, result.stderr); - return result.stdout.trim(); - }; - runGit('init', '-q'); - runGit('config', 'user.name', 'Installer Test'); - runGit('config', 'user.email', 'installer-test@example.invalid'); - runGit('config', 'commit.gpgsign', 'false'); - runGit('remote', 'add', 'origin', 'https://github.com/almide/almide.git'); - await writeFile(join(source, '.gitignore'), 'target/\n'); - await writeFile(join(source, 'compiler.rs'), '// unchanged\n'); - runGit('add', '.'); - runGit('commit', '-qm', 'fixture'); - const revision = runGit('rev-parse', 'HEAD'); - await writeFile(join(root, '.almide-revision'), revision + '\n'); - const realGit = spawnSync('sh', ['-c', 'command -v git'], { encoding: 'utf8' }).stdout.trim(); - await writeFile(join(bin, 'git'), `#!/bin/sh\nif [ "$3" = fetch ]; then exit 0; fi\nexec "${realGit}" "$@"\n`, { mode: 0o755 }); - await writeFile(join(bin, 'cargo'), `#!/bin/sh\nset -eu\necho called > "$TEST_ROOT/cargo-called"\nmkdir -p "$TEST_ROOT/.tools/almide-source/target/release"\nprintf '#!/bin/sh\\necho fake-test-compiler\\n' > "$TEST_ROOT/.tools/almide-source/target/release/almide"\nchmod +x "$TEST_ROOT/.tools/almide-source/target/release/almide"\n`, { mode: 0o755 }); + for (const tool of ['almide', 'almide-verify']) { + await writeFile(join(staging, tool), `#!/bin/sh\necho fake-${tool}\n`, { mode: 0o755 }); + } + const archive = join(root, `${name}.tar.gz`); + const tar = spawnSync('tar', ['-czf', archive, '-C', join(root, 'staging'), name], { encoding: 'utf8' }); + assert.equal(tar.status, 0, tar.stderr); + const digest = createHash('sha256').update(await readFile(archive)).digest('hex'); + const listed = tamper ? digest.replace(/^./, c => (c === '0' ? '1' : '0')) : digest; + await writeFile(join(root, '.almide-release'), `${tag}\n`); + await writeFile(join(root, '.almide-checksums.sha256'), `${listed} ${name}.tar.gz\n`); + // Stub curl: record the URL and copy the fixture archive to the -o target. + await writeFile(join(bin, 'curl'), `#!/bin/sh\nset -eu\nout=\nwhile [ $# -gt 0 ]; do case "$1" in -o) out=$2; shift 2;; -*) shift;; *) echo "$1" > "$TEST_ROOT/curl-url"; shift;; esac; done\ncp "$TEST_ROOT/${name}.tar.gz" "$out"\n`, { mode: 0o755 }); const run = () => spawnSync('bash', ['scripts/install-almide.sh'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: bin + ':' + process.env.PATH, TEST_ROOT: root }, }); - return { root, source, revision, run }; -} - -for (const kind of ['tracked modification', 'untracked file']) { - test(`installer refuses ${kind} without deleting it`, async t => { - const f = await fixture(t); - const edited = join(f.source, kind === 'tracked modification' ? 'compiler.rs' : 'new-file.rs'); - await writeFile(edited, '// preserve this edit\n'); - const result = f.run(); - assert.notEqual(result.status, 0); - assert.match(result.stderr, /modified or untracked/); - assert.equal(await readFile(edited, 'utf8'), '// preserve this edit\n'); - await assert.rejects(readFile(join(f.root, 'cargo-called')), { code: 'ENOENT' }); - await assert.rejects(readFile(join(f.root, '.tools/bin/almide.revision')), { code: 'ENOENT' }); - }); + return { root, name, tag, run }; } -test('installer accepts a clean checkout with ignored build artifacts', async t => { +test('installer installs both tools from a release archive whose checksum matches', async t => { const f = await fixture(t); - await mkdir(join(f.source, 'target')); - await writeFile(join(f.source, 'target/existing-build'), 'keep\n'); const result = f.run(); assert.equal(result.status, 0, result.stderr); - assert.equal(await readFile(join(f.root, '.tools/bin/almide.revision'), 'utf8'), f.revision + '\n'); - assert.equal(await readFile(join(f.source, 'target/existing-build'), 'utf8'), 'keep\n'); + assert.match(result.stdout, /fake-almide/); + assert.equal(await readFile(join(f.root, '.tools/bin/almide.release'), 'utf8'), `${f.tag}\n`); + await access(join(f.root, '.tools/bin/almide-verify')); + assert.equal((await readFile(join(f.root, 'curl-url'), 'utf8')).trim(), + `https://github.com/almide/almide/releases/download/${f.tag}/${f.name}.tar.gz`); +}); + +test('installer refuses an archive with the wrong checksum and installs nothing', async t => { + const f = await fixture(t, { tamper: true }); + const result = f.run(); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Checksum mismatch/); + await assert.rejects(access(join(f.root, '.tools/bin')), { code: 'ENOENT' }); +}); + +test('installer refuses a malformed release tag before downloading', async t => { + const f = await fixture(t, { tag: 'main' }); + const result = f.run(); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Invalid release tag/); + await assert.rejects(access(join(f.root, 'curl-url')), { code: 'ENOENT' }); }); From 64758770e310cfba5eebd29d7be8ca443fc77490 Mon Sep 17 00:00:00 2001 From: O6lvl4 Date: Sun, 4 Oct 2026 17:05:56 +0900 Subject: [PATCH 2/2] Add ConoHa VPS Terraform and record the VPS and release-pin verification providers/conoha/terraform creates one disposable Docker VPS with the Aid-On conohavps provider fork: server, boot volume, key pair, and a security group admitting only TCP 22 from the operator's CIDRs (ConoHa's IPv4v6-SSH group admits SSH from anywhere, so it is not used). Credentials come only from CONOHAVPS_* in the environment; state, tfvars and the lock file are ignored. A static test guards the SSH-only shape. The VPS was created, the Compose example built and passed the shared 18-case contract over an SSH tunnel with both the source-build installer (7m18s) and the release installer (91s, 24s rebuilt), and it was destroyed. Docs now describe the v0.66.0 release pin. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 3 + .gitignore | 8 ++ README.md | 62 +++++++++------ docs/verification.md | 77 ++++++++++++++++--- licenses/README.md | 3 +- providers/azure-container-apps/README.md | 4 +- providers/conoha/README.md | 64 ++++++++++++--- providers/conoha/terraform/main.tf | 50 ++++++++++++ providers/conoha/terraform/outputs.tf | 11 +++ .../conoha/terraform/terraform.tfvars.example | 4 + providers/conoha/terraform/variables.tf | 42 ++++++++++ providers/conoha/terraform/versions.tf | 11 +++ providers/google-cloud-run/README.md | 3 +- tests/provider-config.test.mjs | 19 +++++ 14 files changed, 311 insertions(+), 50 deletions(-) create mode 100644 providers/conoha/terraform/main.tf create mode 100644 providers/conoha/terraform/outputs.tf create mode 100644 providers/conoha/terraform/terraform.tfvars.example create mode 100644 providers/conoha/terraform/variables.tf create mode 100644 providers/conoha/terraform/versions.tf diff --git a/.dockerignore b/.dockerignore index 069a315..f28db08 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,3 +15,6 @@ node_modules **/.env* **/.dev.vars* **/*.private.json +**/.terraform +**/*.tfstate* +**/*.tfvars diff --git a/.gitignore b/.gitignore index 742a1c8..6dedf75 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,11 @@ node_modules/ local.settings.json .azure/ *.private.json +.terraform/ +*.tfstate +*.tfstate.* +*.tfvars +!*.tfvars.example +.terraform.tfstate.lock.info +.terraform.lock.hcl +crash.log diff --git a/README.md b/README.md index 076b0bc..fdb61b1 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,17 @@ every provider. ## Verified scope -Checked on **2026-10-04** against Almide commit -[`852b028a5706801fd008a753bcbdf8b3ea93156f`](https://github.com/almide/almide/commit/852b028a5706801fd008a753bcbdf8b3ea93156f) -(compiler reports `0.66.0 (dev)`). The exact source revision is in -[.almide-revision](.almide-revision). A released `0.66.0` binary is not assumed to -be the same compiler. +The compiler is the official Almide +[`v0.66.0`](https://github.com/almide/almide/releases/tag/v0.66.0) release binary +(`almide 0.66.0 (release, 819bbc74f)`), pinned by [.almide-release](.almide-release) +and the archive checksums in [.almide-checksums.sha256](.almide-checksums.sha256). +Results checked on **2026-10-04**. Most rows below were first established with a +source build of the earlier pin, commit +[`852b028`](https://github.com/almide/almide/commit/852b028a5706801fd008a753bcbdf8b3ea93156f) +(`0.66.0 (dev)`); after the switch, every local suite, Docker/Compose on macOS arm64 +and on the ConoHa VPS, and GitHub Actions were rerun with the release binary. +Cloudflare and Google deployments have not been repeated with it. +See [verification notes](docs/verification.md#compiler-pin-moved-to-the-v0660-release). | Route | Build / bundle | Shared API contract | Live cloud | | --- | --- | --- | --- | @@ -24,27 +30,29 @@ be the same compiler. | macOS arm64 native HTTP | Passed locally with the `--release` installer | 18 cases passed over HTTP | Not applicable | | Wasm + generated JS, Node 24.19.0 | Passed locally | Same 18 cases + 1,000 repeated string calls | Not applicable | | Workers, Wrangler 4.147.0 / local workerd | Dry-run bundle passed; real `wrangler deploy` uploaded | Same 18 cases passed over HTTP locally and on the workers.dev edge | Deployed temporarily to workers.dev, verified, deleted | -| ConoHa Docker / Compose | Image built and Compose started on Docker 29.6.1, linux/arm64 only | Same 18 cases passed against the container | Not deployed | +| ConoHa Docker / Compose | Image built and Compose started on macOS arm64 (Docker 29.6.1) and on a ConoHa VPS, x86_64 (Docker 29.2.1, Compose v5.0.2) | Same 18 cases passed against the container on both | VPS created with [Terraform](providers/conoha/terraform/), verified, destroyed | | Google Cloud Run container | linux/amd64 image built (QEMU on Apple silicon), pushed by digest; `replace --dry-run` and deploy passed | Same 18 cases passed from a VM inside the VPC with an ID token | Deployed temporarily with internal ingress + IAM, verified, deleted | | Azure Container Apps / ECS Fargate | Provider templates and local safety/shape checks passed; image not built for them | Shared native contract passed; provider runtime not run | Not deployed | | AWS Lambda, Node 24 | Source package generated; adapter/config tests passed | 18 common cases, base64/event/HEAD/warm-call checks; staged package executed locally | Not deployed | | Google Cloud Run functions, Node 24 | Source package, local Functions Framework, and managed source build via `deploy.sh --execute` | 18 direct adapter cases; in the cloud, 18 octet-stream cases passed and JSON showed the same 3 framework rejections as locally | Deployed temporarily with internal ingress + IAM, verified, deleted | | Azure Functions v4, Node 24 | Source package, adapter/config tests and actual SDK request objects tested | 18 common cases; Functions host/key enforcement not run | Not deployed | -GitHub Actions (`ubuntu-24.04`, full bootstrap and every reproduction step) has -passed on this branch. See [verification notes](docs/verification.md) for exact -commands and limits. +GitHub Actions (`ubuntu-24.04`, compiler install and every reproduction step) has +passed. See [verification notes](docs/verification.md) for exact commands and limits. ## Quick start -Requires Linux or macOS, Git, Rust **1.99.0**, a C build toolchain, Bash, and Node -**22+**. Local evidence used Linux x86_64 with Node **24.19.0**, and macOS arm64 -with Node **24.21.0** and **22.23.1**. Rust installation is a prerequisite; the -repository does not install it for you. `rust-toolchain.toml` selects 1.99.0 only -when `cargo` is the rustup proxy; a standalone `cargo` earlier on `PATH` ignores it. +Requires Linux (glibc 2.39+, e.g. Ubuntu 24.04 or Debian 13) or macOS, curl, Rust +**1.99.0**, a C build toolchain, Bash, and Node **22+**. Rust is still needed +because native `almide build` emits Rust and compiles it with cargo; the compiler +itself is downloaded. Local evidence used Linux x86_64 with Node **24.19.0**, and +macOS arm64 with Node **24.21.0** and **22.23.1**. Rust installation is a +prerequisite; the repository does not install it for you. `rust-toolchain.toml` +selects 1.99.0 only when `cargo` is the rustup proxy; a standalone `cargo` earlier +on `PATH` ignores it. ```sh -./scripts/install-almide.sh # builds the exact compiler commit; first build is substantial +./scripts/install-almide.sh # downloads the pinned release and checks its sha256 (seconds) npm ci npm run build npm test @@ -84,9 +92,10 @@ npm run dev:workers Container templates use existing infrastructure and private/internal ingress; function examples retain IAM or function-key authentication. They do not create accounts or silently grant caller access. Read [deployment safety and cleanup](docs/deployment-safety.md) -before applying any example. The Cloudflare Workers, Google Cloud Run container and -Cloud Run functions examples have been deployed temporarily for verification and -then deleted; the other providers have not been deployed. +before applying any example. The Cloudflare Workers, Google Cloud Run container, +Cloud Run functions and ConoHa VPS examples have been deployed temporarily for +verification and then deleted; the AWS and Azure examples have not been deployed. +The ConoHa VPS itself is created by the optional [Terraform](providers/conoha/terraform/). ### Prepare and test function packages locally @@ -149,13 +158,16 @@ custom WASI shim or a provider-specific compiler backend. ## Build discipline -The installer refuses modified or untracked compiler source without deleting edits. -Almide is pinned by full commit, Rust by exact version, Wrangler by exact version -and npm lockfile, and GitHub Actions by commit. Docker base images use explicit -version tags but are not digest-locked; this is not yet a fully hermetic build. +The installer downloads the release archive for the current platform and refuses +to unpack or install it unless its sha256 matches the committed checksum. +Almide is pinned by release tag and archive checksum, Rust by exact version, +Wrangler by exact version and npm lockfile, and GitHub Actions by commit. Docker +base images use explicit version tags but are not digest-locked; this is not yet +a fully hermetic build. To move to another release, update `.almide-release` and +copy that release's `almide-checksums.sha256` lines into `.almide-checksums.sha256`. `ALMIDE_BIN=/absolute/path/to/almide npm run build` is available for local compiler -development, but bypasses the default revision-file check; record the source -revision yourself when using it. +development, but bypasses the default release check; record the compiler version +yourself when using it. Generated Wasm/JS, executables, dependencies and local credentials are ignored. Nothing in the default build/test workflow provisions cloud resources or deploys. @@ -168,4 +180,4 @@ Copyright (c) 2026 Aid-On Inc. Almide and other third-party tools and dependencies retain their own licenses. This repository's MIT license does not relicense the pinned Almide compiler, its runtime, generated third-party code, or npm dependencies. See the -[upstream Almide license at the pinned revision](https://github.com/almide/almide/blob/852b028a5706801fd008a753bcbdf8b3ea93156f/LICENSE). +[upstream Almide license at the pinned release](https://github.com/almide/almide/blob/v0.66.0/LICENSE). diff --git a/docs/verification.md b/docs/verification.md index 50177fb..cd410c3 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -2,6 +2,10 @@ Date: 2026-10-04 (UTC) +The compiler pin is now the official `v0.66.0` release binary; see +[Compiler pin moved to the v0.66.0 release](#compiler-pin-moved-to-the-v0660-release). +The sections before it record runs made with a source build of the earlier pin. + ## Toolchain and source - Almide source: `852b028a5706801fd008a753bcbdf8b3ea93156f` @@ -184,11 +188,66 @@ Cloud Run functions: (invalid JSON, body limit, trailing text) failed, matching the local record 4. 403 without a token from the VPC; 404 with a valid token from the internet +## Compiler pin moved to the v0.66.0 release + +Building the compiler from source dominated every build: about 7.5 minutes on an +Apple silicon Mac, and 5m46s of a 7m18s `docker compose build` on a 4-core VPS. +The official `v0.66.0` release (2026-10-03) ships linux x86_64/aarch64 and macOS +binaries with a checksum file, so the pin moved from source commit `852b028` to +that release. Its tag commit `819bbc7` and `852b028` have diverged (30 and 155 +commits apart), so it is a different compiler and was re-verified: + +1. `install-almide.sh` downloads the platform archive and installs it only if its + sha256 matches `.almide-checksums.sha256` (copied from the release's + `almide-checksums.sha256`). 2.4 seconds on macOS arm64 +2. The installer tests stub only the download: a matching archive installs + `almide` and `almide-verify`; a wrong checksum installs nothing; a malformed + tag is refused before any download +3. macOS arm64, Node 24.21.0: `npm test` 117/117, `test:workers` 19/19, + `test:google-framework` 39/39, `test:staged-functions` 38/38. App build 7 s +4. The release binary needs glibc 2.39+, so it fails on Debian bookworm; the + Dockerfile moved to `rust:1.99.0-trixie` / `debian:trixie-slim`. Native + `almide build` still emits Rust and runs cargo (it fails without cargo), so + the build stage keeps Rust +5. macOS arm64 `docker build`: 10 s with cached base images; Compose and the 18 + cases passed, read-only root filesystem and UID 65532 retained +6. ConoHa VPS x86_64 (below): 91 s including base-image pulls, 24 s with + `--no-cache`; 18 cases passed + +The earlier Cloudflare and Google deployments used the source-built compiler and +were not repeated with the release binary. License texts are identical at both pins. + +## ConoHa VPS + +Date: 2026-10-04. Created with [providers/conoha/terraform](../providers/conoha/terraform/) +(Terraform 1.14.9, the Aid-On fork of the conohavps provider at `ff59ace`, built +locally and used through `dev_overrides`), region c3j1. + +1. `terraform plan`: 5 to add (server, boot volume, key pair, security group, one + SSH rule from the operator's /32). Existing servers, keys and groups in the + account were not in the plan and were unchanged afterwards +2. `terraform apply`: 1m10s. `g2l-t-c4m4`, `vmi-docker-29.2-ubuntu-24.04-amd64`: + Ubuntu 24.04.4, x86_64, 4 vCPU, 3.8 GiB, Docker 29.2.1, Compose v5.0.2. The + account's second server was accepted +3. First boot ran unattended upgrades through cloud-init for about 15 minutes; + the build waited for `cloud-init status --wait` +4. ConoHa README commands from a clone of the repository: + - with the source-build installer (main at `42cecaa`): build 7m18s (compiler + 5m46s); `/health`, `/greet` and the 18 cases passed + - with the release installer (`4f72f7b`): build 91 s including base-image + pulls, 24 s with `--no-cache`; the 18 cases passed +5. The 18 cases ran from the Mac through `ssh -L` to the VPS loopback. The app + listened only on `127.0.0.1:8080`; port 8080 on the public address was not + reachable. The container ran as 65532 with a read-only root filesystem and + `CapDrop=[ALL]`; `docker compose down` took 0.5 s +6. `terraform destroy` removed all 5 resources; the VPS existed about 23 minutes + ## Not established -- x86_64 Compose startup or native x86_64 Docker build (the amd64 image was - built and run only under QEMU, and run on Cloud Run) -- A ConoHa VPS installation, ingress, TLS, restart behavior or production load +- Native x86_64 Docker build outside the ConoHa VPS +- ConoHa TLS/reverse proxy, restart behavior, production load, or plans smaller + than `g2l-t-c4m4` +- Cloudflare and Google deployments with the release compiler - Azure Container Apps or ECS Fargate provider validation/deployment - Lambda managed runtime or Azure Functions host execution/authentication - Google Cloud costs, load, cold-start latency or long-running behavior @@ -214,12 +273,12 @@ npm run test:google-framework npm run test:staged-functions ``` -For a future Docker gate, run the Compose commands in the ConoHa README on a -machine with Docker and verify `/health` and `/greet` before marking the route -container-tested. Deployments require a separate, explicit decision. +For a Docker gate, run the Compose commands in the ConoHa README on a machine with +Docker and verify `/health` and `/greet`. Deployments require a separate, +explicit decision. ## Upstream references -- [HTTP server semantics at the pin](https://github.com/almide/almide/blob/852b028a5706801fd008a753bcbdf8b3ea93156f/docs/stdlib/http.md) -- [Generated JS host at the pin](https://github.com/almide/almide/blob/852b028a5706801fd008a753bcbdf8b3ea93156f/src/cli/js_host.rs) -- [JS host contract at the pin](https://github.com/almide/almide/blob/852b028a5706801fd008a753bcbdf8b3ea93156f/docs/wasm/WASM-OUTPUT.md) +- [HTTP server semantics at the pin](https://github.com/almide/almide/blob/v0.66.0/docs/stdlib/http.md) +- [Generated JS host at the pin](https://github.com/almide/almide/blob/v0.66.0/src/cli/js_host.rs) +- [JS host contract at the pin](https://github.com/almide/almide/blob/v0.66.0/docs/wasm/WASM-OUTPUT.md) diff --git a/licenses/README.md b/licenses/README.md index 6e74c71..c647f9b 100644 --- a/licenses/README.md +++ b/licenses/README.md @@ -1,7 +1,8 @@ # Third-party notices The files `Almide-MIT.txt` and `Almide-APACHE.txt` reproduce the license texts from -[Almide at the compiler revision pinned by this repository](https://github.com/almide/almide/tree/852b028a5706801fd008a753bcbdf8b3ea93156f). +[Almide at the release pinned by this repository](https://github.com/almide/almide/tree/v0.66.0) +(identical to those at the earlier source pin `852b028`). Almide is offered under MIT or Apache-2.0, at the recipient's option. These notices accompany the generated Wasm packages; our root MIT license does not replace them. diff --git a/providers/azure-container-apps/README.md b/providers/azure-container-apps/README.md index 9d65e33..7511bc7 100644 --- a/providers/azure-container-apps/README.md +++ b/providers/azure-container-apps/README.md @@ -95,8 +95,8 @@ Use the returned SHA-256 digest, removing only the `sha256:` prefix, for the tag. Confirm that this digest identifies the tested amd64 image. ARM's length constraints do not prove the digest exists or validate its architecture. -The shared image runs as UID/GID 65532 and uses `PORT=8080`. Its compiler build is -substantial; build on a suitably sized machine. Explicit `--platform` matters +The shared image runs as UID/GID 65532 and uses `PORT=8080`. Its build downloads +the pinned compiler and compiles only the app. Explicit `--platform` matters on ARM laptops. Base images currently use version tags, so pinning the deployed image digest does not make source rebuilds fully hermetic. diff --git a/providers/conoha/README.md b/providers/conoha/README.md index 04b9693..430ba48 100644 --- a/providers/conoha/README.md +++ b/providers/conoha/README.md @@ -1,20 +1,60 @@ # ConoHa VPS: native container route -Status: native executable tested on Linux locally. The image and the Compose -commands below were run locally on linux/arm64 (Docker 29.6.1) and passed the -shared 18-case HTTP contract. An x86_64 image and a ConoHa VPS deployment have -**not** been run yet. This directory is a reproducible setup candidate, not a -claim of hosted support. No ConoHa SDK is required. +Status: on 2026-10-04 a VPS was created with the [Terraform](#optional-a-disposable-vps-with-terraform) +below (`g2l-t-c4m4`, Docker 29.2.1, Compose v5.0.2, Ubuntu 24.04.4, x86_64). The +Compose commands below built the image on the VPS, and the shared 18-case HTTP +contract passed through an SSH tunnel; port 8080 was not reachable from the +internet. The VPS was then destroyed. The same commands also passed on macOS +arm64 (Docker 29.6.1). TLS, a reverse proxy, restart behavior and load were not +tested. No ConoHa SDK is required. ## Prerequisites - An existing Linux VPS, compatible Docker Engine and Compose plugin, and a deliberate plan for TLS and ingress. Do not create resources just to run tests. -- Build on a matching Linux architecture; the first local proof was x86_64. -- Sufficient build resources. The Almide compiler build embeds Wasmtime and is much - heavier than the tiny deployed app; build elsewhere and transfer an image if - the VPS is small. ConoHa's documented 1 GiB template minimum is not a build-RAM - recommendation. +- Build on a matching Linux architecture. +- The image build downloads the pinned compiler and compiles only the app with + cargo. On a 4-core, 4 GB VPS it took 91 seconds including base-image pulls, and + 24 seconds for a rebuild without layer cache. Smaller plans were not measured. + +## Optional: a disposable VPS with Terraform + +[terraform/](terraform/) creates one new VPS for this example and nothing else: +a `g2l-t-c4m4` server (Linux, hourly billing, 4 cores, 4 GB) from ConoHa's Docker +image, its 100 GB boot volume, an SSH key pair, and a security group that admits +only TCP 22 from the CIDRs you give. ConoHa's `IPv4v6-SSH` group admits SSH from +anywhere, so it is not used. Existing servers, keys and groups in the account are +not read or changed. The server is billed hourly until it is destroyed, stopped or +not; check [ConoHa pricing](https://vps.conoha.jp/pricing/) first. + +It uses the Aid-On fork of the ConoHa provider, which is not on the Terraform +Registry. Build it and point `dev_overrides` at it as its +[setup section](https://github.com/Aid-On/terraform-provider-conohavps#セットアップ) +describes. Credentials are those of a ConoHa API user, given only through the +environment; keep them in a file outside Git: + +```sh +cat > ~/.config/conoha/credentials.env <<'X' +export CONOHAVPS_TENANT_ID='...' +export CONOHAVPS_USER_ID='...' +export CONOHAVPS_PASSWORD='...' +X +chmod 600 ~/.config/conoha/credentials.env +set -a; . ~/.config/conoha/credentials.env; set +a + +cd providers/conoha/terraform +cp terraform.tfvars.example terraform.tfvars # set ssh_allowed_cidrs to your /32 +terraform init +terraform plan +terraform apply +ssh root@$(terraform output -raw ipv4) cloud-init status --wait +``` + +`terraform.tfvars`, state and `.terraform/` are ignored by Git. A new ConoHa +account may refuse a second server (`Number of flavors (plans) allowed per project +is limit`) until ConoHa raises the limit. On first boot the Docker image runs +unattended upgrades, so wait for cloud-init before building. Then run the +commands below on the server, and finish with `terraform destroy`. From the repository root: @@ -30,8 +70,8 @@ docker compose -f providers/conoha/compose.yaml down Compose binds the app only to the host loopback address. For public access, place a TLS reverse proxy in front and configure the intended ConoHa security group and guest firewall yourself. Docker-published ports can bypass UFW; UFW alone is -not proof the app is private. No firewall, TLS, DNS or cloud-account automation is -included here. +not proof the app is private. The optional Terraform creates only the server and +its SSH-only security group; no TLS, DNS or public ingress is included. The runtime image uses a non-root UID, and Compose drops capabilities and makes the root filesystem read-only. These choices are a starting point, not a security diff --git a/providers/conoha/terraform/main.tf b/providers/conoha/terraform/main.tf new file mode 100644 index 0000000..80f55cb --- /dev/null +++ b/providers/conoha/terraform/main.tf @@ -0,0 +1,50 @@ +# One disposable Docker VPS for the Compose example. Everything here is new and +# named after var.name; existing servers, keys and security groups are left alone. + +data "conohavps_flavor" "plan" { name = var.flavor } +data "conohavps_image" "docker" { name = var.image } + +resource "conohavps_keypair" "admin" { + name = "${var.name}-admin" + public_key = file(pathexpand(var.ssh_public_key)) +} + +# SSH from the operator's addresses and nothing else inbound. compose.yaml +# publishes the app on host loopback only, so it is reached through SSH. +resource "conohavps_securitygroup" "ssh" { + name = "${var.name}-ssh" + description = "SSH from operator CIDRs only" +} + +resource "conohavps_securitygroup_rule" "ssh" { + for_each = toset(var.ssh_allowed_cidrs) + securitygroup_id = conohavps_securitygroup.ssh.id + direction = "ingress" + ethertype = strcontains(each.value, ":") ? "IPv6" : "IPv4" + protocol = "tcp" + port_range_min = 22 + port_range_max = 22 + remote_ip_prefix = each.value +} + +resource "conohavps_volume" "boot" { + name = "${var.name}-boot" + size = 100 # the plan's own boot storage; 200 or 500 is billed as added storage + volume_type = "c3j1-ds02-boot" + image_ref = data.conohavps_image.docker.id +} + +resource "conohavps_instance" "host" { + instance_name_tag = var.name + flavor_id = data.conohavps_flavor.plan.id + block_device = [{ uuid = conohavps_volume.boot.id }] + key_name = conohavps_keypair.admin.name + security_group = [{ name = conohavps_securitygroup.ssh.name }] + power_state = "ACTIVE" + depends_on = [conohavps_securitygroup_rule.ssh] +} + +locals { + # The global address: addresses also lists additional IPs (add-) and local networks (local-). + ipv4 = [for net, addrs in conohavps_instance.host.addresses : [for a in addrs : a.addr if a.version == 4][0] if startswith(net, "ext-")][0] +} diff --git a/providers/conoha/terraform/outputs.tf b/providers/conoha/terraform/outputs.tf new file mode 100644 index 0000000..e33b221 --- /dev/null +++ b/providers/conoha/terraform/outputs.tf @@ -0,0 +1,11 @@ +output "ipv4" { + value = local.ipv4 +} + +output "ssh" { + value = "ssh root@${local.ipv4}" +} + +output "instance_id" { + value = conohavps_instance.host.id +} diff --git a/providers/conoha/terraform/terraform.tfvars.example b/providers/conoha/terraform/terraform.tfvars.example new file mode 100644 index 0000000..57bd6dd --- /dev/null +++ b/providers/conoha/terraform/terraform.tfvars.example @@ -0,0 +1,4 @@ +# Copy to terraform.tfvars (ignored by Git) and fill in. +ssh_allowed_cidrs = ["203.0.113.10/32"] +# ssh_public_key = "~/.ssh/id_ed25519.pub" +# flavor = "g2l-t-c4m4" diff --git a/providers/conoha/terraform/variables.tf b/providers/conoha/terraform/variables.tf new file mode 100644 index 0000000..0812e6b --- /dev/null +++ b/providers/conoha/terraform/variables.tf @@ -0,0 +1,42 @@ +variable "name" { + description = "Name of the server and the prefix of everything created with it" + type = string + default = "almide-cloud-example" + validation { + condition = can(regex("^[a-z][a-z0-9-]{2,40}$", var.name)) + error_message = "Use 3-41 lowercase letters, digits or hyphens." + } +} + +variable "flavor" { + # The verified plan. The image build compiles only the app (the compiler is + # downloaded); smaller plans were not measured. + description = "ConoHa plan by flavor name: g2l-t-c4m4 is Linux, hourly billing, 4 cores, 4 GB" + type = string + default = "g2l-t-c4m4" +} + +variable "image" { + description = "ConoHa Docker application image (Docker Engine and Compose on Ubuntu 24.04)" + type = string + default = "vmi-docker-29.2-ubuntu-24.04-amd64" +} + +variable "ssh_public_key" { + description = "Path of the public key installed for root" + type = string + default = "~/.ssh/id_ed25519.pub" +} + +variable "ssh_allowed_cidrs" { + # No default on purpose. ConoHa's IPv4v6-SSH group admits SSH from anywhere; + # this configuration admits only the given sources. + description = "Source CIDRs allowed to reach SSH, for example your own address as /32" + type = list(string) + validation { + condition = length(var.ssh_allowed_cidrs) > 0 && alltrue([ + for c in var.ssh_allowed_cidrs : can(cidrhost(c, 0)) && !contains(["0.0.0.0/0", "::/0"], c) + ]) + error_message = "Give at least one specific CIDR; 0.0.0.0/0 and ::/0 are refused." + } +} diff --git a/providers/conoha/terraform/versions.tf b/providers/conoha/terraform/versions.tf new file mode 100644 index 0000000..458859c --- /dev/null +++ b/providers/conoha/terraform/versions.tf @@ -0,0 +1,11 @@ +# The conohavps provider is the Aid-On fork (https://github.com/Aid-On/terraform-provider-conohavps), +# which is not on the Registry: build it and point dev_overrides at it (README.md). +terraform { + required_version = ">= 1.5" + required_providers { + conohavps = { source = "gmo-internet/conohavps" } + } +} + +# Credentials from CONOHAVPS_TENANT_ID, CONOHAVPS_USER_ID and CONOHAVPS_PASSWORD. +provider "conohavps" {} diff --git a/providers/google-cloud-run/README.md b/providers/google-cloud-run/README.md index b95c1a4..60036d7 100644 --- a/providers/google-cloud-run/README.md +++ b/providers/google-cloud-run/README.md @@ -100,7 +100,8 @@ selects 8080 and HTTP/1. Cloud Run terminates HTTPS before forwarding to it. Do not add TLS inside this container or override `PORT` in the service environment. See the [container contract](https://docs.cloud.google.com/run/docs/container-contract). -The compiler build is much heavier than the deployed program. Docker base tags +The image build downloads the pinned compiler and compiles only the app; under +QEMU on Apple silicon it is still much slower than a native amd64 build. Docker base tags in the shared Dockerfile are not digest-locked; pinning the deployed image fixes the deployed bytes but does not make rebuilding hermetic. Review/scanning of that image and graceful shutdown/load behavior remain operator responsibilities. diff --git a/tests/provider-config.test.mjs b/tests/provider-config.test.mjs index 2785460..c6475b1 100644 --- a/tests/provider-config.test.mjs +++ b/tests/provider-config.test.mjs @@ -128,6 +128,25 @@ test('function deployments preserve IAM/key authentication defaults', async () = }); +test('ConoHa Terraform admits only operator SSH and keeps state and credentials out of Git', async () => { + const read = path => readFile(new URL(`../providers/conoha/terraform/${path}`, import.meta.url), 'utf8'); + const main = await read('main.tf'); + const variables = await read('variables.tf'); + const versions = await read('versions.tf'); + assert.match(versions, /source\s*=\s*"gmo-internet\/conohavps"/); + assert.match(versions, /provider "conohavps" \{\}/, 'credentials come from the environment'); + // Only port 22, only from the given CIDRs, and not ConoHa's world-open SSH group. + assert.equal(main.match(/resource "conohavps_securitygroup_rule"/g).length, 1); + assert.match(main, /port_range_min\s*=\s*22\s*\n\s*port_range_max\s*=\s*22/); + assert.match(main, /remote_ip_prefix\s*=\s*each\.value/); + assert.match(main, /security_group\s*=\s*\[\{ name = conohavps_securitygroup\.ssh\.name \}\]/); + assert.doesNotMatch(main, /IPv4v6-SSH|0\.0\.0\.0\/0|::\/0/); + assert.match(variables, /variable "ssh_allowed_cidrs" \{(?:(?!default\s*=)[\s\S])*?validation/, 'no default CIDR'); + assert.match(variables, /"0\.0\.0\.0\/0", "::\/0"/); + const gitIgnore = await readFile(new URL('../.gitignore', import.meta.url), 'utf8'); + for (const entry of ['.terraform/', '*.tfstate', '*.tfvars', '!*.tfvars.example']) assert.ok(gitIgnore.includes(entry), entry); +}); + test('local credential and tool outputs are excluded from source and Docker context', async () => { const gitIgnore = await readFile(new URL('../.gitignore', import.meta.url), 'utf8'); const dockerIgnore = await readFile(new URL('../.dockerignore', import.meta.url), 'utf8');