diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b060ccd..d5d4cb1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,10 +7,12 @@ on: branches: [master, main] paths: - simplicio.exe + - simplicio-windows-x64.exe - simplicio - simplicio-darwin-x64 - simplicio-linux-x64 - simplicio-update-manifest.json + - distribution/targets.json - .github/workflows/release.yml workflow_dispatch: {} @@ -27,13 +29,18 @@ jobs: shell: pwsh run: echo "version=$((Get-Content simplicio-update-manifest.json | ConvertFrom-Json).version)" >> $env:GITHUB_OUTPUT - name: Stage release assets + # Asset names MUST match distribution/targets.json (the canonical + # target-triplet table) so that install.sh, install.ps1 and + # simplicio-update-manifest.json all resolve the same URLs. + # scripts/verify_distribution_consistency.py enforces this in CI. run: | mkdir -p dist - cp simplicio.exe dist/simplicio-windows-x86_64.exe + cp simplicio-windows-x64.exe dist/simplicio-windows-x64.exe cp simplicio dist/simplicio-macos-arm64 - if (Test-Path simplicio-darwin-x64) { cp simplicio-darwin-x64 dist/simplicio-darwin-x64 } + if (Test-Path simplicio-darwin-x64) { cp simplicio-darwin-x64 dist/simplicio-macos-x64 } if (Test-Path simplicio-linux-x64) { cp simplicio-linux-x64 dist/simplicio-linux-x64 } cp simplicio-update-manifest.json dist/ + cp distribution/targets.json dist/ cp SHA256SUMS dist/ || true - name: Create or update release uses: softprops/action-gh-release@v2 @@ -51,9 +58,10 @@ jobs: prerelease: false make_latest: "true" files: | - dist/simplicio-windows-x86_64.exe + dist/simplicio-windows-x64.exe dist/simplicio-macos-arm64 - dist/simplicio-darwin-x64 + dist/simplicio-macos-x64 dist/simplicio-linux-x64 dist/simplicio-update-manifest.json + dist/targets.json dist/SHA256SUMS diff --git a/INSTALL.md b/INSTALL.md index e64fdd5..0b5f163 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -20,16 +20,43 @@ powershell -c "irm https://raw.githubusercontent.com/wesleysimplicio/simplicio/m ### Manual Download Download the binary for your platform from the -[releases](https://github.com/wesleysimplicio/simplicio/releases): +[releases](https://github.com/wesleysimplicio/simplicio/releases). Asset names +are canonical — see [`distribution/targets.json`](distribution/targets.json), +the single source of truth used by the release workflow, both installers and +`simplicio-update-manifest.json`: -| Platform | File | -|----------|------| -| macOS (Apple Silicon) | `simplicio` | -| macOS (Intel) | `simplicio` | -| Linux x86_64 | `simplicio` | -| Windows x86_64 | `simplicio.exe` | +| Target id | Platform | Asset | +|----------------|------------------------|------------------------------| +| `macos-arm64` | macOS (Apple Silicon) | `simplicio-macos-arm64` | +| `macos-x64` | macOS (Intel) | `simplicio-macos-x64` | +| `linux-x64` | Linux x86_64 | `simplicio-linux-x64` | +| `windows-x64` | Windows x86_64 | `simplicio-windows-x64.exe` | -Place it somewhere on your `PATH` (e.g. `/usr/local/bin` or `~/.local/bin`). +Verify the SHA256 checksum against the `sha256` field for your target in +`simplicio-update-manifest.json` (published alongside each release) before +running the binary. Then place it somewhere on your `PATH` (e.g. +`/usr/local/bin` or `~/.local/bin`), naming it `simplicio` (or `simplicio.exe` +on Windows). + +## Doctor / Uninstall + +Both installers are idempotent and expose a health check and a clean, +data-preserving uninstall — safe to re-run at any time: + +```bash +# macOS / Linux +sh install.sh --doctor +sh install.sh --uninstall +``` + +```powershell +# Windows +pwsh install.ps1 -Doctor +pwsh install.ps1 -Uninstall +``` + +Uninstall only removes the installed binary; it never deletes user data or +config under `~/.simplicio`. ## Verify Installation diff --git a/distribution/targets.json b/distribution/targets.json new file mode 100644 index 0000000..b636145 --- /dev/null +++ b/distribution/targets.json @@ -0,0 +1,43 @@ +{ + "schema": "simplicio.target-triplets/v1", + "$comment": "Single source of truth for platform naming across the ecosystem. Any script, workflow or manifest that needs an asset name for a platform MUST derive it from this table (id -> asset) instead of hard-coding its own convention. scripts/verify_distribution_consistency.py enforces that release.yml, install.sh, install.ps1 and simplicio-update-manifest.json all agree with this table.", + "targets": [ + { + "id": "windows-x64", + "os": "windows", + "arch": "x64", + "rust_triple": "x86_64-pc-windows-msvc", + "asset": "simplicio-windows-x64.exe", + "installer": "install.ps1", + "manifest_target": "windows-x64" + }, + { + "id": "macos-arm64", + "os": "macos", + "arch": "arm64", + "rust_triple": "aarch64-apple-darwin", + "asset": "simplicio-macos-arm64", + "installer": "install.sh", + "manifest_target": "macos-arm64" + }, + { + "id": "macos-x64", + "os": "macos", + "arch": "x64", + "rust_triple": "x86_64-apple-darwin", + "asset": "simplicio-macos-x64", + "installer": "install.sh", + "manifest_target": "macos-x64", + "legacy_aliases": ["simplicio-darwin-x64"] + }, + { + "id": "linux-x64", + "os": "linux", + "arch": "x64", + "rust_triple": "x86_64-unknown-linux-gnu", + "asset": "simplicio-linux-x64", + "installer": "install.sh", + "manifest_target": "linux-x64" + } + ] +} diff --git a/install.ps1 b/install.ps1 index 29c7a6c..d3dcd2e 100755 --- a/install.ps1 +++ b/install.ps1 @@ -1,36 +1,110 @@ #!/usr/bin/env pwsh -# install.ps1 — Install the simplicio binary on Windows +# install.ps1 — Install/update/uninstall/doctor the simplicio binary on Windows # # Usage: # powershell -c "irm https://raw.githubusercontent.com/wesleysimplicio/simplicio/master/install.ps1 | iex" +# pwsh install.ps1 -Doctor +# pwsh install.ps1 -Uninstall # # Environment variables: -# SIMPLICIO_VERSION - pin a specific version (default: latest) -# SIMPLICIO_BIN_DIR - custom install directory +# SIMPLICIO_VERSION - pin a specific version (default: latest) +# SIMPLICIO_BIN_DIR - custom install directory +# SIMPLICIO_ALLOW_UNVERIFIED - "1" to proceed even if no checksum is +# published for this target (default: refuse) +# +# Asset naming follows distribution/targets.json (the canonical target +# triplet table for the whole ecosystem) — target "windows-x64", asset +# "simplicio-windows-x64.exe". Any drift between this script, the release +# workflow and simplicio-update-manifest.json is caught by +# scripts/verify_distribution_consistency.py in CI. param( - [string]$Version = "" + [string]$Version = "", + [switch]$Doctor, + [switch]$Uninstall ) $ErrorActionPreference = "Stop" $Repo = "wesleysimplicio/simplicio" $BinName = "simplicio.exe" +$Target = "windows-x64" +$Asset = "simplicio-windows-x64.exe" -# Detect architecture. Published Windows asset is simplicio-windows-x64. -$Arch = "x64" - -Write-Host "==> simplicio installer for Windows ($Arch)" - -# Determine install dir if ($env:SIMPLICIO_BIN_DIR) { $InstallDir = $env:SIMPLICIO_BIN_DIR } else { $InstallDir = "$env:USERPROFILE\.local\bin" } +$DestPath = Join-Path $InstallDir $BinName + +function Test-InPath([string]$dir) { + return ($env:Path -split ";") -contains $dir +} + +# ─── -Doctor: idempotent, read-only health check ─────────────────────────── +if ($Doctor) { + Write-Host "==> simplicio doctor" + $ok = $true + + if (Test-Path $DestPath) { + Write-Host " [OK] binary present: $DestPath" + } else { + Write-Host " [FAIL] binary missing at $DestPath" + $ok = $false + } + + if (Test-InPath $InstallDir) { + Write-Host " [OK] $InstallDir is on PATH" + } else { + Write-Host " [WARN] $InstallDir is not on PATH (current session)" + } + + if (Test-Path $DestPath) { + try { + $verOut = & $DestPath version 2>&1 | Out-String + Write-Host " [OK] binary runs: $($verOut.Trim())" + } catch { + Write-Host " [FAIL] binary present but failed to execute: $($_.Exception.Message)" + $ok = $false + } + } + + if ($ok) { + Write-Host "" + Write-Host " ✓ simplicio looks healthy" + exit 0 + } else { + Write-Host "" + Write-Host " ✗ simplicio has problems — re-run the installer" + exit 1 + } +} + +# ─── -Uninstall: idempotent removal, safe to run repeatedly ──────────────── +if ($Uninstall) { + Write-Host "==> simplicio uninstall" + if (Test-Path $DestPath) { + Remove-Item -Force $DestPath + Write-Host " ✓ removed $DestPath" + } else { + Write-Host " ✓ already removed (nothing at $DestPath)" + } + # Data/config is intentionally preserved (idempotent, non-destructive + # uninstall) — user data under ~/.simplicio is never touched here. + Write-Host " ✓ user data under `$env:USERPROFILE\.simplicio was preserved" + Write-Host "" + Write-Host " Note: if you added $InstallDir to your PowerShell profile's" + Write-Host " `$env:Path, remove that line manually — this script never" + Write-Host " edits your profile." + exit 0 +} + +# ─── Install / update ─────────────────────────────────────────────────────── +Write-Host "==> simplicio installer for Windows ($Target)" + New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null -# Determine version if (-not $Version -and $env:SIMPLICIO_VERSION) { $Version = $env:SIMPLICIO_VERSION } @@ -39,31 +113,89 @@ if (-not $Version) { try { $latest = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -ErrorAction Stop $Version = $latest.tag_name - Write-Host " ✓ latest version: $Version" + Write-Host " - latest version: $Version" } catch { $Version = "latest" - Write-Host " ⚠ could not determine latest, using '$Version'" + Write-Host " ! could not determine latest, using '$Version'" } } -# Download. Release assets are raw binaries named simplicio-windows-x64 -# (no tarball). Try the pinned tag then the 'latest' redirect. -$Asset = "simplicio-windows-$Arch" -$DestPath = Join-Path $InstallDir $BinName -$urls = @( - "https://github.com/$Repo/releases/download/$Version/$Asset", - "https://github.com/$Repo/releases/latest/download/$Asset" -) -$dlOk = $false -foreach ($u in $urls) { - Write-Host "==> downloading $u" - try { - Invoke-WebRequest -Uri $u -OutFile $DestPath -UseBasicParsing -ErrorAction Stop - if ((Test-Path $DestPath) -and ((Get-Item $DestPath).Length -gt 0)) { $dlOk = $true; break } - } catch { Write-Host " ⚠ failed: $($_.Exception.Message)" } +# Resolve the release base (pinned tag, or the 'latest' redirect) once so the +# binary and the manifest we verify it against come from the same place. +if ($Version -eq "latest") { + $ReleaseBase = "https://github.com/$Repo/releases/latest/download" +} else { + $ReleaseBase = "https://github.com/$Repo/releases/download/$Version" } -if (-not $dlOk) { - Write-Error "Download failed for $Asset. Get it at https://github.com/wesleysimplicio/simplicio/releases/latest" + +# ─── Fetch update manifest for checksum verification ─────────────────────── +$ManifestUrl = "$ReleaseBase/simplicio-update-manifest.json" +$Manifest = $null +try { + Write-Host "==> fetching update manifest for verification..." + $Manifest = Invoke-RestMethod -Uri $ManifestUrl -ErrorAction Stop +} catch { + Write-Host " ! could not fetch update manifest: $($_.Exception.Message)" +} + +$ExpectedSha256 = $null +$ExpectedSigned = $false +if ($Manifest) { + $artifact = $Manifest.artifacts | Where-Object { $_.target -eq $Target } | Select-Object -First 1 + if ($artifact) { + $ExpectedSha256 = $artifact.sha256 + $ExpectedSigned = [bool]$artifact.signed + } +} + +if (-not $ExpectedSha256) { + if ($env:SIMPLICIO_ALLOW_UNVERIFIED -eq "1") { + Write-Host " ! no published checksum for target '$Target' — proceeding UNVERIFIED (SIMPLICIO_ALLOW_UNVERIFIED=1)" + } else { + Write-Error "Refusing to install: no published SHA256 checksum for target '$Target' in the update manifest. Set SIMPLICIO_ALLOW_UNVERIFIED=1 to override at your own risk." + exit 1 + } +} elseif (-not $ExpectedSigned) { + Write-Host " ! checksum will be verified, but this artifact is not cryptographically signed yet (ed25519 signing not wired for $Target — see issue #5)" +} + +# ─── Download to a staging file, verify, then atomically swap in ────────── +$DownloadUrl = "$ReleaseBase/$Asset" +$StagingPath = "$DestPath.download-$([guid]::NewGuid().ToString('N')).tmp" + +Write-Host "==> downloading $DownloadUrl" +try { + Invoke-WebRequest -Uri $DownloadUrl -OutFile $StagingPath -UseBasicParsing -ErrorAction Stop +} catch { + Write-Error "Download failed for $Asset from $DownloadUrl : $($_.Exception.Message)" + if (Test-Path $StagingPath) { Remove-Item -Force $StagingPath -ErrorAction SilentlyContinue } + exit 1 +} + +if (-not (Test-Path $StagingPath) -or (Get-Item $StagingPath).Length -eq 0) { + Write-Error "Downloaded file is missing or empty: $StagingPath" + if (Test-Path $StagingPath) { Remove-Item -Force $StagingPath -ErrorAction SilentlyContinue } + exit 1 +} + +if ($ExpectedSha256) { + $actualSha256 = (Get-FileHash -Path $StagingPath -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actualSha256 -ne $ExpectedSha256.ToLowerInvariant()) { + Remove-Item -Force $StagingPath -ErrorAction SilentlyContinue + Write-Error "Checksum mismatch for $Asset. Expected $ExpectedSha256, got $actualSha256. Refusing to install a tampered or corrupt binary." + exit 1 + } + Write-Host " ✓ SHA256 verified: $actualSha256" +} + +# Atomic swap: rename into place on the same volume so there is never a +# window where $DestPath is a half-written file, and re-running this script +# (idempotent update) never leaves stale .tmp files behind on success. +try { + Move-Item -Force -Path $StagingPath -Destination $DestPath +} catch { + Remove-Item -Force $StagingPath -ErrorAction SilentlyContinue + Write-Error "Could not move verified binary into place: $($_.Exception.Message)" exit 1 } Write-Host " ✓ installed: $DestPath" @@ -73,21 +205,23 @@ try { $output = & $DestPath version 2>&1 | Out-String Write-Host " ✓ simplicio is ready!" } catch { - Write-Host " ⚠ binary installed but verification failed" + Write-Host " ! binary installed but verification failed" } # PATH hint -if ($env:Path -notlike "*$InstallDir*") { +if (-not (Test-InPath $InstallDir)) { Write-Host "" - Write-Host " ⚠ $InstallDir is not in PATH" + Write-Host " ! $InstallDir is not in PATH" Write-Host " Add it to your PowerShell profile:" Write-Host " `$env:Path += `";$InstallDir`"" Write-Host "" } Write-Host "" -Write-Host " ✓ simplicio $Version (windows-$Arch) installed successfully" +Write-Host " ✓ simplicio $Version (windows-x64) installed successfully" Write-Host "" -Write-Host " Run: simplicio chat 'hello' --repo ." -Write-Host " REPL: simplicio chat --repl --repo ." +Write-Host " Run: simplicio chat 'hello' --repo ." +Write-Host " REPL: simplicio chat --repl --repo ." +Write-Host " Doctor: pwsh install.ps1 -Doctor" +Write-Host " Remove: pwsh install.ps1 -Uninstall" Write-Host "" diff --git a/install.sh b/install.sh index dea3749..42b2d59 100755 --- a/install.sh +++ b/install.sh @@ -1,26 +1,31 @@ #!/usr/bin/env sh -# install.sh — Simplicio Agent: instalador completo e unificado +# install.sh — Simplicio Agent: instalador completo e unificado (macOS/Linux) # # Um comando. Tudo instalado. Zero configuração. # # curl -fsSL https://raw.githubusercontent.com/wesleysimplicio/simplicio/master/install.sh | sh # -# Instala: -# 1. simplicio binary (Rust runtime) -# 2. simplicio-agent (Hermes Turbo + Tami) -# 3. Desktop app (Electron) -# 4. Wake word "Simplicio" (escuta 24/7) -# 5. Áudio STT + TTS (fala e ouve) -# 6. Tami ativa (consciência emocional) -# 7. Cron: Tami aparece no chat a cada 30min +# Idempotent subcommands: +# sh install.sh --doctor # health check, safe to re-run +# sh install.sh --uninstall # removes the binary, preserves user data # -# Tudo pronto pra usar. Só falar "Simplicio" e começar. +# Environment variables: +# SIMPLICIO_VERSION - pin a specific version (default: latest) +# SIMPLICIO_BIN_DIR - custom install directory +# SIMPLICIO_ALLOW_UNVERIFIED - "1" to proceed even if no checksum is +# published for this target (default: refuse) +# +# Asset naming follows distribution/targets.json (the canonical target +# triplet table for the whole ecosystem): id "macos-arm64" -> asset +# "simplicio-macos-arm64", id "macos-x64" -> "simplicio-macos-x64", id +# "linux-x64" -> "simplicio-linux-x64". Drift between this script, the +# release workflow and simplicio-update-manifest.json is caught by +# scripts/verify_distribution_consistency.py in CI. set -eu REPO="wesleysimplicio/simplicio" GITHUB="https://github.com/$REPO" -RAW="https://raw.githubusercontent.com/$REPO/master" BIN_NAME="simplicio" AGENT_PKG="simplicio-agent" @@ -28,7 +33,6 @@ GREEN='\033[0;32m' CYAN='\033[0;36m' YELLOW='\033[1;33m' RED='\033[0;31m' -BOLD='\033[1m' NC='\033[0m' info() { printf "${CYAN}==>${NC} %s\n" "$*"; } @@ -36,7 +40,89 @@ ok() { printf "${GREEN} ✓${NC} %s\n" "$*"; } warn() { printf "${YELLOW} ⚠${NC} %s\n" "$*"; } err() { printf "${RED} ✗${NC} %s\n" "$*"; exit 1; } -# ─── Banner ────────────────────────────────────────────────────────────────── +BIN_DIR="${SIMPLICIO_BIN_DIR:-$HOME/.local/bin}" +DEST_PATH="$BIN_DIR/$BIN_NAME" + +# ─── Detect platform (canonical os/arch naming, matches distribution/targets.json) ── +detect_platform() { + case "$(uname -m)" in + x86_64|amd64) ARCH="x64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) err "Arquitetura não suportada: $(uname -m)" ;; + esac + + case "$(uname -s)" in + Darwin) OS="macos" ;; + Linux) OS="linux" ;; + *) err "Sistema não suportado: $(uname -s)" ;; + esac +} + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + err "Precisa de sha256sum ou shasum para verificar a integridade do download" + fi +} + +# ─── --doctor: idempotent, read-only health check ────────────────────────── +run_doctor() { + info "simplicio doctor" + status=0 + + if [ -x "$DEST_PATH" ]; then + ok "binário presente: $DEST_PATH" + else + warn "binário ausente em $DEST_PATH" + status=1 + fi + + case ":$PATH:" in + *":$BIN_DIR:"*) ok "$BIN_DIR está no PATH" ;; + *) warn "$BIN_DIR não está no PATH (sessão atual)" ;; + esac + + if [ -x "$DEST_PATH" ]; then + if "$DEST_PATH" version >/dev/null 2>&1; then + ok "binário executa corretamente" + else + warn "binário presente mas falhou ao executar" + status=1 + fi + fi + + if [ "$status" -eq 0 ]; then + ok "simplicio está saudável" + else + err "simplicio tem problemas — rode o instalador novamente" + fi + exit "$status" +} + +# ─── --uninstall: idempotent removal, safe to run repeatedly ────────────── +run_uninstall() { + info "simplicio uninstall" + if [ -e "$DEST_PATH" ]; then + rm -f "$DEST_PATH" + ok "removido $DEST_PATH" + else + ok "já estava removido (nada em $DEST_PATH)" + fi + # Dados do usuário são preservados intencionalmente (uninstall idempotente + # e não-destrutivo) — ~/.simplicio nunca é tocado aqui. + ok "dados do usuário em \$HOME/.simplicio foram preservados" + warn "se você adicionou $BIN_DIR ao PATH no seu ~/.zshrc ou ~/.bashrc, remova a linha manualmente" + exit 0 +} + +case "${1:-}" in + --doctor) detect_platform; run_doctor ;; + --uninstall) run_uninstall ;; +esac + printf "${GREEN}" cat << "EOF" ╔══════════════════════════════════════╗ @@ -48,48 +134,104 @@ printf "${NC}" echo "" # ─── 1. Detect platform ────────────────────────────────────────────────────── - ARCH="" - case "$(uname -m)" in - x86_64|amd64) ARCH="x86_64" ;; - aarch64|arm64) ARCH="arm64" ;; - *) err "Arquitetura não suportada: $(uname -m)" ;; - esac - - OS="" - case "$(uname -s)" in - Darwin) OS="darwin" ;; - Linux) OS="linux" ;; - *) err "Sistema não suportado: $(uname -s)" ;; - esac - - info "Plataforma detectada: $OS-$ARCH" - -# ─── 2. Instalar simplicio binary ──────────────────────────────────────────── +detect_platform +info "Plataforma detectada: $OS-$ARCH" + +# ─── 2. Instalar simplicio binary (staged download + SHA256 + atomic swap) ── info "Instalando Simplicio Runtime..." -BIN_DIR="${SIMPLICIO_BIN_DIR:-$HOME/.local/bin}" mkdir -p "$BIN_DIR" -if command -v "$BIN_DIR/$BIN_NAME" >/dev/null 2>&1; then - ok "$BIN_NAME já instalado em $BIN_DIR/$BIN_NAME" +if [ -x "$DEST_PATH" ]; then + ok "$BIN_NAME já instalado em $DEST_PATH" else VERSION="${SIMPLICIO_VERSION:-latest}" + ASSET="simplicio-$OS-$ARCH" if [ "$VERSION" = "latest" ]; then - DOWNLOAD_URL="$GITHUB/releases/latest/download/simplicio-$OS-$ARCH" + RELEASE_BASE="$GITHUB/releases/latest/download" else - DOWNLOAD_URL="$GITHUB/releases/download/$VERSION/simplicio-$OS-$ARCH" + RELEASE_BASE="$GITHUB/releases/download/$VERSION" + fi + DOWNLOAD_URL="$RELEASE_BASE/$ASSET" + MANIFEST_URL="$RELEASE_BASE/simplicio-update-manifest.json" + + fetch() { + # fetch + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$1" -o "$2" + elif command -v wget >/dev/null 2>&1; then + wget -q "$1" -O "$2" + else + err "Precisa de curl ou wget para baixar" + fi + } + + TARGET_ID="$OS-$ARCH" + EXPECTED_SHA256="" + SIGNED="false" + MANIFEST_TMP="$(mktemp)" + trap 'rm -f "$MANIFEST_TMP"' EXIT + if fetch "$MANIFEST_URL" "$MANIFEST_TMP" 2>/dev/null; then + if command -v python3 >/dev/null 2>&1; then + EXPECTED_SHA256="$(python3 -c " +import json,sys +try: + m = json.load(open('$MANIFEST_TMP')) + for a in m.get('artifacts', []): + if a.get('target') == '$TARGET_ID': + print(a.get('sha256') or '') + print('true' if a.get('signed') else 'false') + break +except Exception: + pass +" 2>/dev/null | sed -n '1p')" + SIGNED="$(python3 -c " +import json +try: + m = json.load(open('$MANIFEST_TMP')) + for a in m.get('artifacts', []): + if a.get('target') == '$TARGET_ID': + print('true' if a.get('signed') else 'false') + break +except Exception: + pass +" 2>/dev/null)" + fi + fi + + if [ -z "$EXPECTED_SHA256" ]; then + if [ "${SIMPLICIO_ALLOW_UNVERIFIED:-}" = "1" ]; then + warn "sem checksum publicado para o alvo '$TARGET_ID' — prosseguindo SEM VERIFICAÇÃO (SIMPLICIO_ALLOW_UNVERIFIED=1)" + else + err "recusando instalar: nenhum SHA256 publicado no manifest para o alvo '$TARGET_ID'. Defina SIMPLICIO_ALLOW_UNVERIFIED=1 para prosseguir por sua conta e risco." + fi + elif [ "$SIGNED" != "true" ]; then + warn "checksum será verificado, mas este artefato ainda não é assinado (ed25519 não configurado para $TARGET_ID — ver issue #5)" fi info "Baixando de $DOWNLOAD_URL ..." - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$DOWNLOAD_URL" -o "$BIN_DIR/$BIN_NAME" - elif command -v wget >/dev/null 2>&1; then - wget -q "$DOWNLOAD_URL" -O "$BIN_DIR/$BIN_NAME" - else - err "Precisa de curl ou wget para baixar" + STAGING_PATH="$DEST_PATH.download-$$.tmp" + fetch "$DOWNLOAD_URL" "$STAGING_PATH" + + if [ ! -s "$STAGING_PATH" ]; then + rm -f "$STAGING_PATH" + err "download falhou ou arquivo vazio: $DOWNLOAD_URL" + fi + + if [ -n "$EXPECTED_SHA256" ]; then + ACTUAL_SHA256="$(sha256_of "$STAGING_PATH")" + if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then + rm -f "$STAGING_PATH" + err "checksum não confere para $ASSET. esperado $EXPECTED_SHA256, obtido $ACTUAL_SHA256. Recusando instalar binário corrompido ou adulterado." + fi + ok "SHA256 verificado: $ACTUAL_SHA256" fi - chmod +x "$BIN_DIR/$BIN_NAME" - ok "Simplicio Runtime instalado em $BIN_DIR/$BIN_NAME" + chmod +x "$STAGING_PATH" + # Swap atômico: mv no mesmo filesystem nunca deixa $DEST_PATH parcialmente + # escrito, e reexecutar este script (update idempotente) não deixa .tmp + # órfãos em caso de sucesso. + mv -f "$STAGING_PATH" "$DEST_PATH" + ok "Simplicio Runtime instalado em $DEST_PATH" fi # Adiciona ao PATH se não estiver @@ -158,11 +300,11 @@ ok "Tami configurada em $TAMI_CONFIG" # ─── 5. Configurar cron da Tami ────────────────────────────────────────────── info "Configurando Tami para aparecer no chat a cada 30min..." # Verifica se o simplicio tem suporte a cron -if command -v "$BIN_DIR/$BIN_NAME" >/dev/null 2>&1; then +if command -v "$DEST_PATH" >/dev/null 2>&1; then # Testa se o runtime tem o comando de cron - "$BIN_DIR/$BIN_NAME" cron list 2>/dev/null && { + "$DEST_PATH" cron list 2>/dev/null && { # Tenta registrar via simplicio - "$BIN_DIR/$BIN_NAME" cron add --name "Tami" --schedule "every 30m" --deliver "origin" 2>/dev/null || { + "$DEST_PATH" cron add --name "Tami" --schedule "every 30m" --deliver "origin" 2>/dev/null || { warn "Não foi possível registrar cron automaticamente. Tami será ativada manualmente." } } || { @@ -180,6 +322,7 @@ printf "${GREEN}║ 💚 Tami está cuidando de você printf "${GREEN}║ 🎤 Diga \"Simplicio\" para começar ║${NC}\n" printf "${GREEN}║ 🖥️ Desktop: simplicio desktop ║${NC}\n" printf "${GREEN}║ 📱 Chat: simplicio agent start ║${NC}\n" +printf "${GREEN}║ 🩺 Doctor: sh install.sh --doctor ║${NC}\n" printf "${GREEN}║ ║${NC}\n" printf "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}\n" echo "" diff --git a/scripts/verify_distribution_consistency.py b/scripts/verify_distribution_consistency.py index f9c3094..11f1276 100644 --- a/scripts/verify_distribution_consistency.py +++ b/scripts/verify_distribution_consistency.py @@ -5,6 +5,9 @@ - canonical branch install URLs (`master` in this repo) - release/version source-of-truth mismatches - stale or contradictory public-beta claims +- target-triplet naming drift between distribution/targets.json (the + canonical table), the release workflow, the update manifest and the + installers (issue #5) Exit code: - 0: no hard failures (warnings may still be printed) @@ -80,9 +83,85 @@ def iter_install_reference_files() -> Iterable[Path]: yield path +def check_target_triplet_consistency(findings: list[Finding]) -> None: + """Enforce that distribution/targets.json is the single source of truth + for asset naming across release.yml, the update manifest and both + installers. This is the concrete regression guard for issue #5's + "tabela canonica de target triplets" acceptance criterion. + """ + targets_path = ROOT / "distribution/targets.json" + if not targets_path.exists(): + findings.append(Finding("ERROR", "distribution/targets.json (canonical target-triplet table) is missing.")) + return + + table = load_json(targets_path) + targets = table.get("targets", []) + if not targets: + findings.append(Finding("ERROR", "distribution/targets.json has no targets defined.")) + return + + release_yml = read_text(ROOT / ".github/workflows/release.yml") + install_ps1 = read_text(ROOT / "install.ps1") + install_sh = read_text(ROOT / "install.sh") + manifest = load_json(ROOT / "simplicio-update-manifest.json") + manifest_by_target = {a.get("target"): a for a in manifest.get("artifacts", [])} + + offenders: list[str] = [] + + for t in targets: + target_id = t["id"] + asset = t["asset"] + installer = t.get("installer") + manifest_target = t.get("manifest_target", target_id) + + # release.yml must reference the canonical asset name for anything it stages. + if f"dist/{asset}" not in release_yml: + offenders.append(f"release.yml does not stage dist/{asset} (target {target_id})") + + # The installer responsible for this target must use the canonical asset name. + if installer == "install.ps1" and asset not in install_ps1: + offenders.append(f"install.ps1 does not reference canonical asset {asset} (target {target_id})") + if installer == "install.sh": + if t["os"] not in install_sh or t["arch"] not in install_sh: + offenders.append( + f"install.sh does not map os={t['os']!r}/arch={t['arch']!r} to canonical asset {asset} (target {target_id})" + ) + + # If the manifest already publishes this target, its artifact name must match. + artifact = manifest_by_target.get(manifest_target) + if artifact and artifact.get("artifact") != asset: + offenders.append( + f"manifest artifact for target {manifest_target!r} is {artifact.get('artifact')!r}, expected canonical {asset!r}" + ) + + if offenders: + findings.append(Finding("ERROR", "target-triplet drift detected: " + "; ".join(offenders))) + else: + findings.append( + Finding("OK", f"release.yml, installers and manifest all agree with distribution/targets.json ({len(targets)} targets).") + ) + + # Unsigned-but-checksummed artifacts are allowed as an interim step, but + # must say so explicitly rather than silently claiming full verification. + unsigned = [ + a["target"] + for a in manifest.get("artifacts", []) + if a.get("sha256") and not a.get("signed", False) + ] + if unsigned: + findings.append( + Finding( + "WARN", + "artifacts published with checksum but no signature yet (interim, see issue #5): " + ", ".join(unsigned), + ) + ) + + def main() -> int: findings: list[Finding] = [] + check_target_triplet_consistency(findings) + version_md = read_text(ROOT / "VERSION.md") if "Use `master` branch only" not in version_md: findings.append(Finding("ERROR", "VERSION.md no longer declares `master` as canonical branch.")) diff --git a/simplicio-update-manifest.json b/simplicio-update-manifest.json index d2666c6..c348309 100644 --- a/simplicio-update-manifest.json +++ b/simplicio-update-manifest.json @@ -20,7 +20,38 @@ "sha256": "931bc6d8f45c1b1e586070f8f5ac4a762861bc9157c70f75aaa4ebfad8ff27bb", "signature": "ed25519:2omwTRLIpznbAjJJfCLKopWRjQWQPegU1bQKFGKQztgcg/ux3YGQWIBw9oOMzzMvd6E4kVe8vWvhU6+hDhfKAg==", "signature_algorithm": "ed25519", + "signed": true, "url": "https://github.com/wesleysimplicio/simplicio/releases/download/v3.5.2/simplicio-macos-arm64" + }, + { + "target": "windows-x64", + "artifact": "simplicio-windows-x64.exe", + "sha256": "ff1a1b6e30f809cf3afc28554ba385f51d1574a41438fcbbdc786267a8ba480c", + "signature": null, + "signature_algorithm": null, + "signed": false, + "signing_note": "checksum verified from committed SHA256SUMS; ed25519 signing infra not yet wired for this target — see issue #5.", + "url": "https://github.com/wesleysimplicio/simplicio/releases/download/v3.5.2/simplicio-windows-x64.exe" + }, + { + "target": "macos-x64", + "artifact": "simplicio-macos-x64", + "sha256": "7f1035be4cc2cf6a5053d894159622b233ca0fad00a5215f9ece3772fe462c42", + "signature": null, + "signature_algorithm": null, + "signed": false, + "signing_note": "checksum verified from committed SHA256SUMS (legacy asset simplicio-darwin-x64); ed25519 signing infra not yet wired for this target — see issue #5.", + "url": "https://github.com/wesleysimplicio/simplicio/releases/download/v3.5.2/simplicio-macos-x64" + }, + { + "target": "linux-x64", + "artifact": "simplicio-linux-x64", + "sha256": "84d86fb1760c11e1e123c013174a712faff62468639f68a0173d53b6cbe99f29", + "signature": null, + "signature_algorithm": null, + "signed": false, + "signing_note": "checksum verified from committed SHA256SUMS; ed25519 signing infra not yet wired for this target — see issue #5.", + "url": "https://github.com/wesleysimplicio/simplicio/releases/download/v3.5.2/simplicio-linux-x64" } ] }