diff --git a/.env.example b/.env.example index b7f3b008f..924146613 100644 --- a/.env.example +++ b/.env.example @@ -417,9 +417,9 @@ IMAGE_TOOLS_DEBUG=false # Default STT provider is "local" (faster-whisper) — runs on your machine, no API key needed. # Install with: pip install faster-whisper # Model downloads automatically on first use (~150 MB for "base"). -# To use cloud providers instead, set GROQ_API_KEY or VOICE_TOOLS_OPENAI_KEY above. -# Provider priority: local > groq > openai -# Configure in config.yaml: stt.provider: local | groq | openai +# To use cloud providers instead, set GROQ_API_KEY, VOICE_TOOLS_OPENAI_KEY, or ELEVENLABS_API_KEY above. +# Provider priority: local > groq > openai > mistral > xai > elevenlabs +# Configure in config.yaml: stt.provider: local | groq | openai | mistral | xai | elevenlabs # ============================================================================= # STT ADVANCED OVERRIDES (optional) @@ -427,10 +427,12 @@ IMAGE_TOOLS_DEBUG=false # Override default STT models per provider (normally set via stt.model in config.yaml) # STT_GROQ_MODEL=whisper-large-v3-turbo # STT_OPENAI_MODEL=whisper-1 +# STT_ELEVENLABS_MODEL=scribe_v2 # Override STT provider endpoints (for proxies or self-hosted instances) # GROQ_BASE_URL=https://api.groq.com/openai/v1 # STT_OPENAI_BASE_URL=https://api.openai.com/v1 +# ELEVENLABS_STT_BASE_URL=https://api.elevenlabs.io/v1 # ============================================================================= # MICROSOFT TEAMS INTEGRATION diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml new file mode 100644 index 000000000..3fc4f2b07 --- /dev/null +++ b/.github/workflows/build-windows-installer.yml @@ -0,0 +1,100 @@ +name: Build Windows Installer + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + # Gate: workflow_dispatch is already restricted to users with write access, + # but we want ADMIN-only. Explicitly check the triggering actor's repo + # permission via the API and fail fast for anyone below admin. + authorize: + name: Authorize (admins only) + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check actor is a repo admin + env: + GH_TOKEN: ${{ github.token }} + ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + perm=$(gh api \ + "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" \ + --jq '.permission') + echo "Actor '${ACTOR}' has permission: ${perm}" + if [ "${perm}" != "admin" ]; then + echo "::error::'${ACTOR}' is not a repo admin (permission=${perm}). Refusing to build/sign." + exit 1 + fi + echo "Authorized: '${ACTOR}' is an admin." + + build: + name: Hermes-Setup.exe + needs: authorize + runs-on: windows-latest + timeout-minutes: 30 + permissions: + contents: read + # Required for OIDC auth to Azure (azure/login federated credentials). + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: npm + + - name: Install npm dependencies + run: npm ci + + - name: Setup Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + + - name: Cache Rust targets + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: apps/bootstrap-installer/src-tauri + + - name: Build installer + run: npm run tauri:build + working-directory: apps/bootstrap-installer + + - name: Azure login (OIDC) + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Sign Hermes-Setup.exe with Azure Artifact Signing + uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2 + with: + endpoint: ${{ vars.AZURE_SIGNING_ENDPOINT }} + signing-account-name: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ vars.AZURE_SIGNING_CERTIFICATE_PROFILE }} + # Sign both the raw exe and the bundled NSIS installer. + files-folder: ${{ github.workspace }}\apps\bootstrap-installer\src-tauri\target\release + files-folder-filter: exe + files-folder-recurse: true + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + + - name: Upload NSIS installer + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: Hermes-Setup-installer + path: apps/bootstrap-installer/src-tauri/target/release/bundle/nsis/*.exe + + - name: Upload raw exe + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: Hermes-Setup-exe + path: apps/bootstrap-installer/src-tauri/target/release/Hermes-Setup.exe diff --git a/.github/workflows/contributor-check.yml b/.github/workflows/contributor-check.yml index 939215ed4..de38fcaae 100644 --- a/.github/workflows/contributor-check.yml +++ b/.github/workflows/contributor-check.yml @@ -3,11 +3,9 @@ name: Contributor Attribution Check on: pull_request: branches: [main] - paths: - # Only run when code files change (not docs-only PRs) - - '*.py' - - '**/*.py' - - '.github/workflows/contributor-check.yml' + # No paths filter — the job must always run so the required check + # reports a status (path-gated workflows leave checks "pending" forever + # when no matching files change, which blocks merge). permissions: contents: read @@ -20,7 +18,21 @@ jobs: with: fetch-depth: 0 # Full history needed for git log + - name: Check if relevant files changed + id: filter + run: | + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + CHANGED=$(git diff --name-only "$BASE"..."$HEAD" -- '*.py' '**/*.py' '.github/workflows/contributor-check.yml' || true) + if [ -n "$CHANGED" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + echo "No Python files changed, skipping attribution check." + fi + - name: Check for unmapped contributor emails + if: steps.filter.outputs.run == 'true' run: | # Get the merge base between this PR and main MERGE_BASE=$(git merge-base origin/main HEAD) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 7dd0c799f..972956293 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -26,6 +26,10 @@ on: permissions: contents: read + # Needed so the arm64 job can push/pull its registry-backed build cache + # to ghcr.io (cache-to/cache-from type=registry). See the build-arm64 + # job for why registry cache replaced the gha cache on that arch. + packages: write # Concurrency: push/release runs are NEVER cancelled so every merge gets # its own image. PR runs reuse a PR-scoped group with @@ -196,11 +200,34 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - # Build once, load into the local daemon for smoke testing. PR arm64 - # builds deliberately avoid the gha cache: cold-cache arm64 builds can - # outlive GitHub's short-lived Azure cache SAS token, then fail while - # reading or writing cache blobs before the smoke test can run. - - name: Build image (arm64, smoke test, uncached PR) + # Log in to ghcr.io so the registry-backed build cache below can be + # read (cache-from) on every event and written (cache-to) on + # push/release. Uses the workflow's GITHUB_TOKEN, which is valid for + # the whole job — unlike the gha cache backend's short-lived Azure SAS + # token, which expired mid-build on slow cold-cache arm64 runs and + # crashed the build before the smoke test (the reason the gha cache + # was removed from arm64 PRs in the first place). + - name: Log in to ghcr.io (build cache) + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Build once, load into the local daemon for smoke testing. + # + # PR builds use the registry-backed cache READ-ONLY (cache-from only): + # they pull warm layers pushed by the most recent main build but never + # write, so rapid PR pushes don't race on cache writes or pollute the + # cache ref. This restores warm-cache speed to arm64 PR builds (which + # were running fully uncached and were ~45% slower than amd64, making + # them the job most often cancelled on supersede). + # + # Registry cache (type=registry on ghcr.io) is used instead of the gha + # cache that previously broke here: its credential is the job-lifetime + # GITHUB_TOKEN, not a short-lived SAS token, so the cold-build-outlives- + # token failure mode cannot recur. + - name: Build image (arm64, smoke test, cache read-only PR) if: github.event_name == 'pull_request' uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: @@ -211,9 +238,11 @@ jobs: tags: ${{ env.IMAGE_NAME }}:test build-args: | HERMES_GIT_SHA=${{ github.sha }} + cache-from: type=registry,ref=ghcr.io/nousresearch/hermes-agent:buildcache-arm64 - # Main/release builds still use the per-arch gha cache so the digest - # push below can reuse layers from this smoke-test build. + # Main/release builds read AND write the registry cache so the digest + # push below reuses layers from this smoke-test build, and so the next + # PR/main build starts warm. - name: Build image (arm64, smoke test, cached publish) if: github.event_name != 'pull_request' uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 @@ -225,8 +254,8 @@ jobs: tags: ${{ env.IMAGE_NAME }}:test build-args: | HERMES_GIT_SHA=${{ github.sha }} - cache-from: type=gha,scope=docker-arm64 - cache-to: type=gha,mode=max,scope=docker-arm64 + cache-from: type=registry,ref=ghcr.io/nousresearch/hermes-agent:buildcache-arm64 + cache-to: type=registry,ref=ghcr.io/nousresearch/hermes-agent:buildcache-arm64,mode=max - name: Smoke test image uses: ./.github/actions/hermes-smoke-test @@ -253,8 +282,8 @@ jobs: build-args: | HERMES_GIT_SHA=${{ github.sha }} outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=gha,scope=docker-arm64 - cache-to: type=gha,mode=max,scope=docker-arm64 + cache-from: type=registry,ref=ghcr.io/nousresearch/hermes-agent:buildcache-arm64 + cache-to: type=registry,ref=ghcr.io/nousresearch/hermes-agent:buildcache-arm64,mode=max - name: Export digest if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'release' diff --git a/.github/workflows/nix-lockfile-fix.yml b/.github/workflows/nix-lockfile-fix.yml index 68fab8605..36b86f18c 100644 --- a/.github/workflows/nix-lockfile-fix.yml +++ b/.github/workflows/nix-lockfile-fix.yml @@ -6,8 +6,8 @@ on: paths: - 'ui-tui/package-lock.json' - 'ui-tui/package.json' - - 'web/package-lock.json' - - 'web/package.json' + - 'apps/dashboard/package-lock.json' + - 'apps/dashboard/package.json' workflow_dispatch: inputs: pr_number: @@ -28,7 +28,7 @@ concurrency: jobs: # ── Auto-fix on main ─────────────────────────────────────────────── # Fires when a push to main touches package.json or package-lock.json - # in ui-tui/ or web/. Runs fix-lockfiles and pushes the hash + # in ui-tui/ or apps/dashboard/. Runs fix-lockfiles and pushes the hash # update commit directly to main so Nix builds never stay broken. # # Safety invariants: @@ -110,7 +110,7 @@ jobs: # run recompute from the correct package-lock state. pkg_changed="$(git diff --name-only "$BASE_SHA"..origin/main -- \ 'ui-tui/package-lock.json' 'ui-tui/package.json' \ - 'web/package-lock.json' 'web/package.json' || true)" + 'apps/dashboard/package-lock.json' 'apps/dashboard/package.json' || true)" if [ -n "$pkg_changed" ]; then echo "::warning::Package files changed since hash computation — aborting; a fresh run will recompute" exit 0 diff --git a/.github/workflows/supply-chain-audit.yml b/.github/workflows/supply-chain-audit.yml index 2f727e8d2..3309de78d 100644 --- a/.github/workflows/supply-chain-audit.yml +++ b/.github/workflows/supply-chain-audit.yml @@ -3,15 +3,9 @@ name: Supply Chain Audit on: pull_request: types: [opened, synchronize, reopened] - paths: - - '**/*.py' - - '**/*.pth' - - '**/setup.py' - - '**/setup.cfg' - - '**/sitecustomize.py' - - '**/usercustomize.py' - - '**/__init__.pth' - - 'pyproject.toml' + # No paths filter — the jobs must always run so required checks + # report a status (path-gated workflows leave checks "pending" forever + # when no matching files change, which blocks merge). permissions: pull-requests: write @@ -27,8 +21,44 @@ permissions: # advisory-only workflow instead. jobs: + # ── Path filter (shared by both scan and dep-bounds) ─────────────── + changes: + runs-on: ubuntu-latest + outputs: + # True when any file the scanner cares about changed in this PR + scan: ${{ steps.filter.outputs.scan }} + # True when pyproject.toml changed in this PR + deps: ${{ steps.filter.outputs.deps }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Check for relevant file changes + id: filter + run: | + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + SCAN_FILES=$(git diff --name-only "$BASE"..."$HEAD" -- \ + '*.py' '**/*.py' '*.pth' '**/*.pth' \ + 'setup.py' 'setup.cfg' \ + 'sitecustomize.py' 'usercustomize.py' '__init__.pth' \ + 'pyproject.toml' || true) + if [ -n "$SCAN_FILES" ]; then + echo "scan=true" >> "$GITHUB_OUTPUT" + else + echo "scan=false" >> "$GITHUB_OUTPUT" + fi + DEPS_FILES=$(git diff --name-only "$BASE"..."$HEAD" -- 'pyproject.toml' || true) + if [ -n "$DEPS_FILES" ]; then + echo "deps=true" >> "$GITHUB_OUTPUT" + else + echo "deps=false" >> "$GITHUB_OUTPUT" + fi + scan: name: Scan PR for critical supply chain risks + needs: changes + if: needs.changes.outputs.scan == 'true' runs-on: ubuntu-latest steps: - name: Checkout @@ -147,10 +177,24 @@ jobs: echo "::error::CRITICAL supply chain risk patterns detected in this PR. See the PR comment for details." exit 1 + # Gate: reports success when scan was skipped (no relevant files changed). + # This ensures the required check always gets a status. + scan-gate: + name: Scan PR for critical supply chain risks + needs: changes + # always() so the gate still reports SUCCESS even if `changes` fails/is + # skipped — without it, a failed dependency would leave the required + # check unreported (i.e. "pending"), the exact failure mode this fixes. + if: always() && needs.changes.outputs.scan != 'true' + runs-on: ubuntu-latest + steps: + - run: echo "No supply-chain-relevant files changed, skipping scan." + dep-bounds: name: Check PyPI dependency upper bounds + needs: changes + if: needs.changes.outputs.deps == 'true' runs-on: ubuntu-latest - if: contains(github.event.pull_request.changed_files_url, 'pyproject.toml') || true steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -211,3 +255,16 @@ jobs: run: | echo "::error::PyPI dependencies without upper bounds detected. Add