diff --git a/install.ps1 b/install.ps1 index 9eee3dc..d5d4253 100755 --- a/install.ps1 +++ b/install.ps1 @@ -41,9 +41,12 @@ $MarketplacePlugins = @("simplicio", "simplicio-loop", "simplicio-prompt", "simp $Target = "windows-x64" $Asset = "simplicio-windows-x64.exe" $Ed25519PublicKey = "2RoVWAoqA/DtDkT5PZdzQYIP82zFskQqJx4S1w06Wok=" -$Ed25519HelperUrl = "https://raw.githubusercontent.com/wesleysimplicio/simplicio/master/scripts/verify_ed25519.py" -$Ed25519HelperSha256 = "f03a0719dd557ddea27dc4cf1456d6f06a47b9056505e4d4b8453090697600d0" -$PinnedPublicKey = ([string]$Ed25519PublicKey).Trim() +$Ed25519HelperUrl = "https://raw.githubusercontent.com/wesleysimplicio/simplicio/master/scripts/verify_ed25519.py" +$Ed25519HelperSha256 = "f03a0719dd557ddea27dc4cf1456d6f06a47b9056505e4d4b8453090697600d0" +$PublicRouteRef = "cc9950025baf823cdecc657228ff1e89d7701e7e" +$PublicRouteUrl = "https://raw.githubusercontent.com/$Repo/$PublicRouteRef/codex/mcp-route.ps1" +$PublicRouteSha256 = "fea2b06c95c9f75bf17fc46b603c8e3817aa6f680af468fa73c066210970c89c" +$PinnedPublicKey = ([string]$Ed25519PublicKey).Trim() $script:Ed25519VerifyError = "" $SimplicioMcpUrl = if ($env:SIMPLICIO_MCP_URL) { $env:SIMPLICIO_MCP_URL } else { "http://127.0.0.1:8787/mcp" } @@ -189,7 +192,7 @@ function Report-LoginState { Write-Warning "Google login missing or entitlement inactive; run: `"$DestPath`" auth login" } -function Test-McpToolSurface([string]$BinaryPath) { +function Test-McpToolSurface([string]$BinaryPath) { if (-not (Test-Path $BinaryPath)) { return $false } try { $env:SIMPLICIO_MCP_URL = $SimplicioMcpUrl @@ -209,6 +212,33 @@ function Invoke-NativeHostCommand([string]$Command, [string[]]$Arguments) { } } +function Sync-PublicRouteOverlay { + $hookDir = Join-Path $PurgeDir "hooks" + $hookPath = Join-Path $hookDir "mcp-route.ps1" + if (Test-Path $hookPath) { + $currentHash = (Get-FileHash -Algorithm SHA256 -Path $hookPath).Hash.ToLowerInvariant() + if ($currentHash -eq $PublicRouteSha256) { return $true } + } + + New-Item -ItemType Directory -Force -Path $hookDir | Out-Null + $hookTemp = Join-Path $hookDir (".mcp-route.ps1.download-$PID") + try { + Invoke-WebRequest -Uri $PublicRouteUrl -OutFile $hookTemp -UseBasicParsing + $downloadHash = (Get-FileHash -Algorithm SHA256 -Path $hookTemp).Hash.ToLowerInvariant() + $downloadText = Get-Content -Raw -Path $hookTemp + if ($downloadHash -ne $PublicRouteSha256 -or + $downloadText -notmatch 'simplicio-hook-version: 3240-v11') { + return $false + } + Move-Item -Force -Path $hookTemp -Destination $hookPath + return $true + } catch { + return $false + } finally { + if (Test-Path $hookTemp) { Remove-Item -Force $hookTemp -ErrorAction SilentlyContinue } + } +} + function Test-AnyPath([string[]]$Paths) { foreach ($path in $Paths) { if (-not [string]::IsNullOrWhiteSpace($path) -and (Test-Path $path)) { return $true } @@ -650,12 +680,18 @@ if (-not (Test-RuntimeReleaseContract $DestPath)) { Write-Host " ✓ Runtime release contract verified" # ─── Register MCP and native hooks for every detected client ────────────── -if (Test-McpToolSurface $DestPath) { - Write-Host " ✓ MCP and hooks registered automatically for detected clients" -} else { - Write-Error "Runtime installed, but automatic MCP/hooks registration failed: $DestPath mcp register --binary $DestPath --json" - exit 1 -} +if (Test-McpToolSurface $DestPath) { + Write-Host " ✓ MCP and hooks registered automatically for detected clients" +} else { + Write-Error "Runtime installed, but automatic MCP/hooks registration failed: $DestPath mcp register --binary $DestPath --json" + exit 1 +} +if (Sync-PublicRouteOverlay) { + Write-Host " ✓ verified public v11 hook reconciled after Runtime registration" +} else { + Write-Error "Runtime registered MCP, but the public v11 hook could not be verified and activated" + exit 1 +} # Host packages add skills, commands and host-specific lifecycle behavior on # top of Runtime MCP registration. Every detected host with a documented diff --git a/install.sh b/install.sh index e7e386b..793eb38 100755 --- a/install.sh +++ b/install.sh @@ -36,6 +36,9 @@ GITHUB="https://github.com/$REPO" ED25519_PUBLIC_KEY="2RoVWAoqA/DtDkT5PZdzQYIP82zFskQqJx4S1w06Wok=" ED25519_HELPER_URL="https://raw.githubusercontent.com/$REPO/master/scripts/verify_ed25519.py" ED25519_HELPER_SHA256="f03a0719dd557ddea27dc4cf1456d6f06a47b9056505e4d4b8453090697600d0" +PUBLIC_ROUTE_REF="cc9950025baf823cdecc657228ff1e89d7701e7e" +PUBLIC_ROUTE_URL="https://raw.githubusercontent.com/$REPO/$PUBLIC_ROUTE_REF/codex/mcp-route.sh" +PUBLIC_ROUTE_SHA256="0f3a6a32c6f224fb3aedea5336f60c5c63917b134113d4c4e524e8ae7b4a31a4" BIN_NAME="simplicio" MARKETPLACE_PLUGINS="simplicio simplicio-loop simplicio-prompt simplicio-sprint simplicio-hermes" @@ -98,6 +101,33 @@ sha256_of() { fi } +reconcile_public_route_overlay() { + hook_dir="$PURGE_DIR/hooks" + hook_path="$hook_dir/mcp-route.sh" + + if [ -f "$hook_path" ] && [ "$(sha256_of "$hook_path")" = "$PUBLIC_ROUTE_SHA256" ]; then + return 0 + fi + + mkdir -p "$hook_dir" || return 1 + hook_tmp="$hook_dir/.mcp-route.sh.download-$$" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$PUBLIC_ROUTE_URL" -o "$hook_tmp" || { rm -f "$hook_tmp"; return 1; } + elif command -v wget >/dev/null 2>&1; then + wget -q "$PUBLIC_ROUTE_URL" -O "$hook_tmp" || { rm -f "$hook_tmp"; return 1; } + else + return 1 + fi + + if [ "$(sha256_of "$hook_tmp")" != "$PUBLIC_ROUTE_SHA256" ] || + ! grep -q 'simplicio-hook-version: 3240-v11' "$hook_tmp"; then + rm -f "$hook_tmp" + return 1 + fi + chmod 0755 "$hook_tmp" || { rm -f "$hook_tmp"; return 1; } + mv -f "$hook_tmp" "$hook_path" +} + verify_ed25519_signature() { binary_path="$1" signature="$2" @@ -740,6 +770,11 @@ if verify_mcp_tools "$DEST_PATH"; then else err "o Runtime foi instalado, mas o registro automático de MCP/hooks falhou: $DEST_PATH mcp register --binary $DEST_PATH --json" fi +if reconcile_public_route_overlay; then + ok "hook público v11 verificado e reconciliado após o registro do Runtime" +else + err "o Runtime registrou o MCP, mas o hook público v11 não pôde ser verificado e ativado" +fi # Host packages add skills, commands and host-specific lifecycle behavior on # top of Runtime MCP registration. Every detected host with a documented diff --git a/tests/test_codex_install_contract.py b/tests/test_codex_install_contract.py index 71c3607..783a0e7 100644 --- a/tests/test_codex_install_contract.py +++ b/tests/test_codex_install_contract.py @@ -1,5 +1,6 @@ """Static contract checks for the public Codex integration.""" +import hashlib import json import os from pathlib import Path @@ -70,18 +71,29 @@ def test_installers_preserve_stable_login_state_during_upgrades(): assert "auth login" in powershell -def test_windows_installer_migrates_legacy_unix_hooks(): +def test_windows_installer_reconciles_the_pinned_public_hook_after_runtime_registration(): powershell = (ROOT / "install.ps1").read_text(encoding="utf-8") + hook_digest = hashlib.sha256((ROOT / "codex" / "mcp-route.ps1").read_bytes()).hexdigest() assert "Install-CodexRouteHook" not in powershell assert "SIMPLICIO_CODEX_HOOK_REF" not in powershell assert "mcp register --binary $BinaryPath --json" in powershell + assert '$PublicRouteRef = "cc9950025baf823cdecc657228ff1e89d7701e7e"' in powershell + assert f'$PublicRouteSha256 = "{hook_digest}"' in powershell + assert "Sync-PublicRouteOverlay" in powershell + assert powershell.index("Test-McpToolSurface $DestPath") < powershell.index("if (Sync-PublicRouteOverlay)") -def test_unix_installer_migrates_legacy_hook_events(): + +def test_unix_installer_reconciles_the_pinned_public_hook_after_runtime_registration(): shell = (ROOT / "install.sh").read_text(encoding="utf-8") + hook_digest = hashlib.sha256((ROOT / "codex" / "mcp-route.sh").read_bytes()).hexdigest() assert "CODEX_ROUTE_HOOK_URL" not in shell assert "install_codex_route_hook" not in shell assert "SIMPLICIO_CODEX_HOOK_REF" not in shell assert 'mcp register --binary "$binary_path" --json' in shell + assert 'PUBLIC_ROUTE_REF="cc9950025baf823cdecc657228ff1e89d7701e7e"' in shell + assert f'PUBLIC_ROUTE_SHA256="{hook_digest}"' in shell + assert "reconcile_public_route_overlay" in shell + assert shell.index('if verify_mcp_tools "$DEST_PATH"') < shell.index("if reconcile_public_route_overlay") def test_unix_installer_accepts_release_manifest_target_aliases(): shell = (ROOT / "install.sh").read_text(encoding="utf-8")