From ec2997f4c9fdd960a621c05f7c75fcbd7442bbb8 Mon Sep 17 00:00:00 2001 From: g1e2x87 Date: Sun, 26 Jul 2026 05:06:09 -0400 Subject: [PATCH 1/2] feat: align one-click install and Rust 1.97.1 --- .github/CODEOWNERS | 1 - .github/ISSUE_TEMPLATE/bug_report.yml | 6 +- .github/ISSUE_TEMPLATE/config.yml | 6 +- .github/workflows/ci-run.yml | 32 ++- .github/workflows/release.yml | 121 +++++++++-- .github/workflows/sec-audit.yml | 56 +++-- .github/workflows/test-rust-build.yml | 2 +- AGENTS.md | 74 +++---- CHANGELOG.md | 16 ++ Cargo.lock | 2 +- Cargo.toml | 5 +- Dockerfile | 2 +- README.md | 61 ++++-- docs/actions-source-policy.md | 17 ++ docs/channels.md | 3 +- docs/configuration.md | 6 +- docs/evolution.md | 9 +- docs/one-click-bootstrap.md | 156 ++++++++++++++ docs/release-notes-0.8.20.md | 28 +++ docs/router.md | 4 +- examples/custom_channel.rs | 3 +- install.ps1 | 94 +++++++++ install.sh | 279 +++++++++++++++++++++++++ rust-toolchain.toml | 4 +- scripts/bootstrap.sh | 82 ++++---- scripts/ci/installer_contract_gate.sh | 131 ++++++++++++ scripts/ci/rust_toolchain_sync_gate.sh | 34 +++ scripts/release/cut_release_tag.sh | 19 +- src/agent/classifier.rs | 4 +- src/causal_tree/engine.rs | 2 +- src/channels/discord.rs | 2 +- src/channels/email_channel.rs | 2 +- src/channels/irc.rs | 2 +- src/channels/lark.rs | 12 +- src/channels/linq.rs | 2 +- src/channels/mod.rs | 6 +- src/channels/nextcloud_talk.rs | 2 +- src/channels/signal.rs | 2 +- src/channels/slack.rs | 4 +- src/channels/telegram.rs | 4 +- src/channels/wacli.rs | 2 +- src/channels/whatsapp.rs | 2 +- src/chat/dispatcher.rs | 38 ++-- src/chat/mod.rs | 8 +- src/chat/sessions/approval.rs | 6 +- src/chat/sessions/pty.rs | 4 +- src/chat/tui.rs | 4 +- src/config/schema.rs | 9 +- src/daemon/mod.rs | 4 +- src/gateway/api/sessions.rs | 2 +- src/gateway/mod.rs | 56 ++--- src/health/mod.rs | 4 +- src/main.rs | 19 +- src/media/artifact.rs | 2 +- src/media/mod.rs | 2 +- src/memory/vector.rs | 1 + src/nodes/client.rs | 2 +- src/providers/compatible.rs | 2 +- src/providers/mod.rs | 6 +- src/providers/openai_codex.rs | 8 +- src/providers/traits.rs | 2 +- src/runtime/shell_process.rs | 2 +- src/security/pairing.rs | 1 + src/security/policy.rs | 10 +- src/self_system/evolution/analyzer.rs | 4 +- src/skills/mod.rs | 29 ++- src/tools/delegate.rs | 2 +- src/tools/sessions_read_model.rs | 2 +- src/tools/sessions_spawn.rs | 2 +- tests/config_hotreload_authz_e2e.rs | 2 +- tests/int_agent_gateway.rs | 4 +- tests/int_p1_cross_module.rs | 6 +- 72 files changed, 1211 insertions(+), 333 deletions(-) create mode 100644 docs/actions-source-policy.md create mode 100644 docs/one-click-bootstrap.md create mode 100644 docs/release-notes-0.8.20.md create mode 100644 install.ps1 create mode 100755 install.sh create mode 100755 scripts/ci/installer_contract_gate.sh create mode 100755 scripts/ci/rust_toolchain_sync_gate.sh diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e6464d1f..fbdf5ab5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -19,7 +19,6 @@ /AGENTS.md @g1e2x87 /CLAUDE.md @g1e2x87 /CONTRIBUTING.md @g1e2x87 -/docs/pr-workflow.md @g1e2x87 /docs/reviewer-playbook.md @g1e2x87 # Security / CI-CD governance overrides (last-match wins) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index df86b8df..be24ba43 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -16,7 +16,7 @@ body: attributes: label: Summary description: One-line description of the problem. - placeholder: openprx daemon exits immediately when ... + placeholder: prx daemon exits immediately when ... validations: required: true @@ -72,8 +72,8 @@ body: label: Steps to reproduce description: Please provide exact commands/config. placeholder: | - 1. openprx onboard --interactive - 2. openprx daemon + 1. prx onboard --interactive + 2. prx daemon 3. Observe crash in logs render: bash validations: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9b930cfd..fcccc11f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,6 +6,6 @@ contact_links: - name: Contribution guide url: https://github.com/openprx/prx/blob/main/CONTRIBUTING.md about: Please read contribution and PR requirements before opening an issue. - - name: PR workflow & reviewer expectations - url: https://github.com/openprx/prx/blob/main/docs/pr-workflow.md - about: Read risk-based PR tracks, CI gates, and merge criteria before filing feature requests. + - name: Installation and runtime documentation + url: https://github.com/openprx/prx#documentation + about: Review installation, configuration, providers, channels, and other runtime documentation. diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml index bbdba417..420215ae 100644 --- a/.github/workflows/ci-run.yml +++ b/.github/workflows/ci-run.yml @@ -17,6 +17,30 @@ env: CARGO_TERM_COLOR: always jobs: + contracts: + name: Installer and Toolchain Contracts + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Verify Rust toolchain alignment + run: ./scripts/ci/rust_toolchain_sync_gate.sh + - name: Verify one-click installer contract + run: ./scripts/ci/installer_contract_gate.sh + + windows-installer: + name: Windows Installer Contract + runs-on: windows-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Parse PowerShell installer + shell: pwsh + run: | + $source = Get-Content -Raw install.ps1 + [void][scriptblock]::Create($source) + Write-Host "PowerShell installer parsed successfully." + changes: name: Detect Change Scope runs-on: ubuntu-latest @@ -52,7 +76,7 @@ jobs: fetch-depth: 0 - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: - toolchain: 1.93.0 + toolchain: 1.97.1 components: rustfmt, clippy - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Run rust quality gate @@ -78,7 +102,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: - toolchain: 1.93.0 + toolchain: 1.97.1 - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Run tests run: cargo test --locked --verbose @@ -94,7 +118,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: - toolchain: 1.93.0 + toolchain: 1.97.1 - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Build binary (smoke check) run: cargo build --locked --verbose @@ -109,7 +133,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: - toolchain: 1.93.0 + toolchain: 1.97.1 targets: x86_64-pc-windows-msvc - name: Install system dependencies run: choco install protoc --yes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3455563f..11fddb44 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,44 @@ env: CARGO_TERM_COLOR: always jobs: + validate: + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag.outputs.tag }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Determine release tag + id: tag + shell: bash + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + tag="${{ github.event.inputs.tag }}" + else + tag="${GITHUB_REF#refs/tags/}" + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + - name: Verify tag matches Cargo package version + shell: bash + env: + RELEASE_TAG: ${{ steps.tag.outputs.tag }} + run: | + set -euo pipefail + package_version="$( + awk -F'"' ' + /^\[package\]$/ { in_package = 1; next } + /^\[/ { in_package = 0 } + in_package && /^version = / { print $2; exit } + ' Cargo.toml + )" + test "$RELEASE_TAG" = "v$package_version" + - name: Verify installer and toolchain contracts + run: | + ./scripts/ci/rust_toolchain_sync_gate.sh + ./scripts/ci/installer_contract_gate.sh + build: + needs: validate strategy: fail-fast: false matrix: @@ -49,6 +86,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: + toolchain: 1.97.1 targets: ${{ matrix.target }} - name: Ensure target is installed @@ -56,7 +94,11 @@ jobs: - name: Install cross if: matrix.use_cross - run: cargo install cross --git https://github.com/cross-rs/cross + run: >- + cargo install cross + --git https://github.com/cross-rs/cross + --rev 1d07d3f9cc465c435256f1aabc1d18024517891a + --locked - name: Create swap (Linux) if: runner.os == 'Linux' @@ -90,24 +132,34 @@ jobs: - name: Build (cross) if: matrix.use_cross - run: cross build --profile ci --bin prx --bin prx-node --target ${{ matrix.target }} + run: cross build --locked --profile ci --bin prx --bin prx-node --target ${{ matrix.target }} - name: Build (native) if: "!matrix.use_cross" env: CARGO_BUILD_JOBS: "2" - run: cargo build --profile ci --bin prx --bin prx-node --target ${{ matrix.target }} + run: cargo build --locked --profile ci --bin prx --bin prx-node --target ${{ matrix.target }} - name: Package (Unix) if: runner.os != 'Windows' + shell: bash run: | + set -euo pipefail cd target/${{ matrix.target }}/ci chmod +x prx prx-node tar czf ../../../prx-${{ matrix.suffix }}.tar.gz prx tar czf ../../../prx-node-${{ matrix.suffix }}.tar.gz prx-node cd ../../.. - sha256sum prx-${{ matrix.suffix }}.tar.gz > prx-${{ matrix.suffix }}.tar.gz.sha256 - sha256sum prx-node-${{ matrix.suffix }}.tar.gz > prx-node-${{ matrix.suffix }}.tar.gz.sha256 + for archive in \ + prx-${{ matrix.suffix }}.tar.gz \ + prx-node-${{ matrix.suffix }}.tar.gz + do + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$archive" > "$archive.sha256" + else + shasum -a 256 "$archive" > "$archive.sha256" + fi + done - name: Package (Windows) if: runner.os == 'Windows' @@ -117,8 +169,12 @@ jobs: Compress-Archive -Path prx.exe -DestinationPath ../../../prx-${{ matrix.suffix }}.zip Compress-Archive -Path prx-node.exe -DestinationPath ../../../prx-node-${{ matrix.suffix }}.zip cd ../../.. - Get-FileHash prx-${{ matrix.suffix }}.zip -Algorithm SHA256 | Format-List Hash | Out-File prx-${{ matrix.suffix }}.zip.sha256 - Get-FileHash prx-node-${{ matrix.suffix }}.zip -Algorithm SHA256 | Format-List Hash | Out-File prx-node-${{ matrix.suffix }}.zip.sha256 + $prxArchive = "prx-${{ matrix.suffix }}.zip" + $nodeArchive = "prx-node-${{ matrix.suffix }}.zip" + $prxHash = (Get-FileHash $prxArchive -Algorithm SHA256).Hash.ToLowerInvariant() + $nodeHash = (Get-FileHash $nodeArchive -Algorithm SHA256).Hash.ToLowerInvariant() + "$prxHash $prxArchive" | Set-Content -Encoding ascii "$prxArchive.sha256" + "$nodeHash $nodeArchive" | Set-Content -Encoding ascii "$nodeArchive.sha256" - name: Upload artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 @@ -130,7 +186,7 @@ jobs: if-no-files-found: error release: - needs: build + needs: [validate, build] if: success() runs-on: ubuntu-latest steps: @@ -139,6 +195,16 @@ jobs: with: path: artifacts merge-multiple: true + - name: Stage standalone installers + shell: bash + run: | + set -euo pipefail + cp install.sh install.ps1 artifacts/ + ( + cd artifacts + sha256sum install.sh > install.sh.sha256 + sha256sum install.ps1 > install.ps1.sha256 + ) - name: Verify release asset completeness shell: bash run: | @@ -164,27 +230,32 @@ jobs: prx-node-macos-arm64.tar.gz.sha256 prx-node-windows-amd64.zip prx-node-windows-amd64.zip.sha256 + install.sh + install.sh.sha256 + install.ps1 + install.ps1.sha256 ) test "$(find artifacts -maxdepth 1 -type f | wc -l)" -eq "${#expected[@]}" for asset in "${expected[@]}"; do test -s "artifacts/$asset" done - - name: Determine tag - id: tag + - name: Smoke-test Linux release binary + shell: bash + env: + RELEASE_TAG: ${{ needs.validate.outputs.tag }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT - else - echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - fi + set -euo pipefail + mkdir -p release-smoke + tar -xzf artifacts/prx-linux-amd64.tar.gz -C release-smoke prx + test "$(release-smoke/prx --version)" = "prx ${RELEASE_TAG#v}" - name: Create or Update Release uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 with: - tag_name: ${{ steps.tag.outputs.tag }} - name: PRX ${{ steps.tag.outputs.tag }} + tag_name: ${{ needs.validate.outputs.tag }} + name: PRX ${{ needs.validate.outputs.tag }} files: artifacts/* body: | - ## PRX ${{ steps.tag.outputs.tag }} + ## PRX ${{ needs.validate.outputs.tag }} Self-evolving AI assistant framework. @@ -205,3 +276,17 @@ jobs: | macOS | Intel | `prx-node-macos-amd64.tar.gz` | | macOS | Apple Silicon | `prx-node-macos-arm64.tar.gz` | | Windows | x86_64 | `prx-node-windows-amd64.zip` | + + ### One-click install + + Linux/macOS: + + ```bash + curl -fsSL https://github.com/openprx/prx/releases/latest/download/install.sh | sh + ``` + + Windows PowerShell: + + ```powershell + irm https://github.com/openprx/prx/releases/latest/download/install.ps1 | iex + ``` diff --git a/.github/workflows/sec-audit.yml b/.github/workflows/sec-audit.yml index 929801fd..2fd52815 100644 --- a/.github/workflows/sec-audit.yml +++ b/.github/workflows/sec-audit.yml @@ -9,7 +9,9 @@ on: - "src/**" - "crates/**" - "deny.toml" + - "rust-toolchain.toml" - '.cargo/audit.toml' + - '.github/workflows/sec-audit.yml' pull_request: branches: [main] paths: @@ -18,7 +20,9 @@ on: - "src/**" - "crates/**" - "deny.toml" + - "rust-toolchain.toml" - '.cargo/audit.toml' + - '.github/workflows/sec-audit.yml' schedule: - cron: "0 6 * * 1" @@ -28,7 +32,6 @@ concurrency: permissions: contents: read - issues: write env: CARGO_TERM_COLOR: always @@ -39,10 +42,14 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 35 steps: - - uses: actions/checkout@v4 - - uses: rustsec/audit-check@v2.0.0 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: - token: ${{ secrets.GITHUB_TOKEN }} + toolchain: 1.97.1 + - name: Install cargo-audit + run: cargo install cargo-audit --version 0.22.2 --locked + - name: Audit locked dependencies + run: cargo audit deny: name: License & Supply Chain @@ -55,8 +62,8 @@ jobs: - bans licenses sources # S5 P0-4: advisories 分支移除 continue-on-error,发布门真正阻断 steps: - - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2 with: command: check ${{ matrix.checks }} arguments: --all-features @@ -67,11 +74,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@nightly + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # nightly with: + toolchain: nightly components: miri - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Run cargo miri smoke run: MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test --lib @@ -81,11 +89,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.93.0 - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 + - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Install cargo-llvm-cov - run: cargo install cargo-llvm-cov --locked + run: cargo install cargo-llvm-cov --version 0.8.7 --locked - name: Run cargo-llvm-cov run: cargo llvm-cov --workspace --lcov --output-path coverage.lcov @@ -95,11 +105,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.93.0 - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 + - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Install cargo-mutants - run: cargo install cargo-mutants --locked + run: cargo install cargo-mutants --version 27.1.0 --locked - name: List cargo-mutants candidates run: cargo mutants --workspace --list @@ -109,10 +121,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@nightly - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # nightly + with: + toolchain: nightly + - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Install cargo-fuzz - run: cargo install cargo-fuzz --locked + run: cargo install cargo-fuzz --version 0.13.2 --locked - name: Run cargo-fuzz smoke run: cargo +nightly fuzz run parse_tool_call_json -- -max_total_time=60 diff --git a/.github/workflows/test-rust-build.yml b/.github/workflows/test-rust-build.yml index 30220bb3..996424bf 100644 --- a/.github/workflows/test-rust-build.yml +++ b/.github/workflows/test-rust-build.yml @@ -15,7 +15,7 @@ on: toolchain: description: "Rust toolchain channel/version." required: false - default: "stable" + default: "1.97.1" type: string components: description: "Optional rustup components." diff --git a/AGENTS.md b/AGENTS.md index 769efe98..47c766d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,36 +153,29 @@ Treat documentation as a first-class product surface, not a post-merge artifact. Canonical entry points: -- root READMEs: `README.md`, `README.zh-CN.md`, `README.ja.md`, `README.ru.md` -- docs hubs: `docs/README.md`, `docs/README.zh-CN.md`, `docs/README.ja.md`, `docs/README.ru.md` -- unified TOC: `docs/SUMMARY.md` - -Collection indexes (category navigation): - -- `docs/getting-started/README.md` -- `docs/reference/README.md` -- `docs/operations/README.md` -- `docs/security/README.md` -- `docs/hardware/README.md` -- `docs/contributing/README.md` -- `docs/project/README.md` +- root project overview: `README.md` +- installation contract: `docs/one-click-bootstrap.md` +- runtime topics: `docs/configuration.md`, `docs/providers.md`, + `docs/channels.md`, `docs/tools.md` +- release history: `CHANGELOG.md`, `docs/release-notes-*.md` Runtime-contract references (must track behavior changes): -- `docs/commands-reference.md` -- `docs/providers-reference.md` -- `docs/channels-reference.md` -- `docs/config-reference.md` -- `docs/operations-runbook.md` -- `docs/troubleshooting.md` - `docs/one-click-bootstrap.md` +- `docs/configuration.md` +- `docs/providers.md` +- `docs/channels.md` +- `docs/tools.md` +- `docs/remote-nodes.md` +- `docs/web-console.md` Required docs governance rules: - Keep README/hub top navigation and quick routes intuitive and non-duplicative. -- Keep EN/ZH/JA/RU entry-point parity when changing navigation architecture. - Keep proposal/roadmap docs explicitly labeled; avoid mixing proposal text into runtime-contract docs. - Keep project snapshots date-stamped and immutable once superseded by a newer date. +- Do not list missing docs or translations as canonical entry points. Add + language parity requirements only when those entry points exist in-tree. ## 5) Risk Tiers by Path (Review Depth Contract) @@ -208,7 +201,7 @@ When uncertain, classify as higher risk. 5. **Document impact** - Update docs/PR notes for behavior, risk, side effects, and rollback. - If CLI/config/provider/channel behavior changed, update corresponding runtime-contract references. - - If docs entry points changed, keep EN/ZH/JA/RU README + docs-hub navigation aligned. + - If docs entry points changed, keep all in-tree entry points aligned. 6. **Respect queue hygiene** - If stacked PR: declare `Depends on #...`. - If replacing old PR: declare `Supersedes #...`. @@ -293,13 +286,14 @@ Use these rules to keep the trait/factory architecture stable under growth. ### 7.6 Docs System / README / IA Changes -- Treat docs navigation as product UX: preserve clear pathing from README -> docs hub -> SUMMARY -> category index. +- Treat docs navigation as product UX: preserve clear pathing from README to + installation and runtime-topic docs. - Keep top-level nav concise; avoid duplicative links across adjacent nav blocks. -- When runtime surfaces change, update related references (`commands/providers/channels/config/runbook/troubleshooting`). -- Keep multilingual entry-point parity for EN/ZH/JA/RU when nav or key wording changes. +- When runtime surfaces change, update the corresponding installation, + provider, channel, tool, or configuration reference. +- Keep every in-tree translated entry point aligned when nav or key wording changes. - For docs snapshots, add new date-stamped files for new sprints rather than rewriting historical context. - ## 8) Validation Matrix Default local checks for code changes: @@ -324,9 +318,11 @@ Notes: Additional expectations by change type: - **Docs/template-only**: - - run markdown lint and link-integrity checks - - if touching README/docs-hub/SUMMARY/collection indexes, verify EN/ZH/JA/RU navigation parity - - if touching bootstrap docs/scripts, run `bash -n bootstrap.sh scripts/bootstrap.sh scripts/install.sh` + - run markdown lint and link-integrity checks + - if touching README or installation docs, verify every linked in-tree entry point + - if touching installer/bootstrap docs or scripts, run + `sh -n install.sh`, `bash -n scripts/bootstrap.sh`, and + `./scripts/ci/installer_contract_gate.sh` - **Workflow changes**: validate YAML syntax; run workflow lint/sanity checks when available. - **Security/runtime/gateway/tools**: include at least one boundary/failure-mode validation. @@ -350,9 +346,9 @@ Treat privacy and neutrality as merge gates, not best-effort guidelines. - Test names/messages/fixtures must be impersonal and system-focused; avoid first-person or identity-specific language. - If identity-like context is unavoidable, use PRX-scoped roles/labels only (for example: `PRXAgent`, `PRXOperator`, `prx_user`) and avoid real-world personas. - Recommended identity-safe naming palette (use when identity-like context is required): - - actor labels: `PRXAgent`, `PRXOperator`, `PRXMaintainer`, `prx_user` - - service/runtime labels: `prx_bot`, `prx_service`, `prx_runtime`, `prx_node` - - environment labels: `prx_project`, `prx_workspace`, `prx_channel` + - actor labels: `PRXAgent`, `PRXOperator`, `PRXMaintainer`, `prx_user` + - service/runtime labels: `prx_bot`, `prx_service`, `prx_runtime`, `prx_node` + - environment labels: `prx_project`, `prx_workspace`, `prx_channel` - If reproducing external incidents, redact and anonymize all payloads before committing. - Before push, review `git diff --cached` specifically for accidental sensitive strings and identity leakage. @@ -427,19 +423,11 @@ Co-authored-by: Reference docs: - `CONTRIBUTING.md` -- `docs/README.md` -- `docs/SUMMARY.md` -- `docs/docs-inventory.md` -- `docs/commands-reference.md` -- `docs/providers-reference.md` -- `docs/channels-reference.md` -- `docs/config-reference.md` -- `docs/operations-runbook.md` -- `docs/troubleshooting.md` - `docs/one-click-bootstrap.md` -- `docs/pr-workflow.md` -- `docs/reviewer-playbook.md` -- `docs/ci-map.md` +- `docs/configuration.md` +- `docs/providers.md` +- `docs/channels.md` +- `docs/tools.md` - `docs/actions-source-policy.md` ## 10) Anti-Patterns (Do Not) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d2d6ef..6ae66f99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.8.20] - 2026-07-26 + +### Added + +- Added pre-built-first `install.sh` and `install.ps1` entry points with exact + version selection, SHA-256 verification, and atomic binary installation. +- Added installer and Rust toolchain contract gates to normal CI and the release + workflow. + +### Changed + +- Aligned the declared, local, CI, audit, release, and Docker Rust toolchains on + Rust 1.97.1. +- Updated the README and installation/configuration references to the real + `prx` CLI and current release asset names. + ### Fixed - Removed host-shell ACL, command-text policy, OS sandbox, environment clearing, diff --git a/Cargo.lock b/Cargo.lock index b52a8576..505fb551 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4659,7 +4659,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openprx" -version = "0.8.19" +version = "0.8.20" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index 78d94881..53132358 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,13 @@ resolver = "2" [package] name = "openprx" -version = "0.8.19" +version = "0.8.20" edition = "2024" +rust-version = "1.97.1" authors = ["g1e2x87"] license = "MIT OR Apache-2.0" description = "OpenPRX: high-performance Rust-first autonomous agent runtime." -repository = "https://openprx.dev" +repository = "https://github.com/openprx/prx" readme = "README.md" keywords = ["ai", "agent", "cli", "assistant", "chatbot"] categories = ["command-line-utilities", "api-bindings"] diff --git a/Dockerfile b/Dockerfile index 20025761..9e85fe27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.7 # ── Stage 1: Build ──────────────────────────────────────────── -FROM rust:1.93-slim@sha256:9663b80a1621253d30b146454f903de48f0af925c967be48c84745537cd35d8b AS builder +FROM rust:1.97.1-slim@sha256:5c6f46a6e4472ab1ca7ba7d494e6677f2f219ebc02f32025d3986f057635ec9c AS builder WORKDIR /app diff --git a/README.md b/README.md index 28655c4f..bb9e4fdb 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,17 @@ Forked from [ZeroClaw](https://github.com/zeroclaw-labs/zeroclaw) and extended w ## Highlights -- **9 LLM providers** — Anthropic, OpenAI, Google Gemini, GitHub Copilot, Ollama, AWS Bedrock, GLM, OpenAI Codex, and OpenAI-compatible endpoints +- **Broad LLM provider catalog** — Anthropic, OpenAI, Google Gemini, GitHub Copilot, Ollama, AWS Bedrock, GLM, OpenAI Codex, local runtimes, and compatible endpoints - **LLM Router** — heuristic routing (capability + Elo + cost + latency), KNN semantic routing (cold-start guard + 100ms timeout fallback), and Automix low-confidence auto-upgrade - **Causal Tree Engine** — speculative multi-branch prediction with rehearsal, scoring, and circuit breaker; opt-in via `causal_tree.enabled` (disabled by default) -- **19 messaging channels** — Signal, WhatsApp, Telegram, Discord, Slack, Matrix, and more -- **38+ built-in tools** — shell, MCP, memory, scheduling, remote nodes +- **Multi-channel messaging** — Signal, WhatsApp, Telegram, Discord, Slack, Matrix, and more +- **Built-in tools and integrations** — shell, MCP, memory, scheduling, remote nodes, and an integration catalog - **Xin (心) task engine** — autonomous heartbeat scheduler with 3 execution modes (Rust/LLM/Shell), 5 built-in system tasks, SQLite persistence - **Web Console** — browser-based management interface (`console/`) - **Remote Nodes** — control macOS/Linux/Pi devices via `prx-node` agent - **Self-Evolution** — autonomous prompt/memory/strategy improvement with xin-managed scheduling - **Subagent Governance** — concurrency limits, depth control, config inheritance -- **3,500+ tests** — comprehensive test coverage across all modules +- **Extensive automated test suite** — unit, integration, PTY, gateway, migration, and security coverage ### LLM Router Flags @@ -40,43 +40,58 @@ Forked from [ZeroClaw](https://github.com/zeroclaw-labs/zeroclaw) and extended w ## Quick Start +Linux and macOS: + ```bash -# Build -git clone https://github.com/openprx/prx.git && cd prx -cargo build --release --all-features +curl -fsSL https://github.com/openprx/prx/releases/latest/download/install.sh | sh +export PATH="$HOME/.local/bin:$PATH" +prx onboard --interactive +prx daemon +``` -# Setup -cp target/release/openprx /usr/local/bin/ -openprx onboard +Windows PowerShell: -# Run -openprx start +```powershell +irm https://github.com/openprx/prx/releases/latest/download/install.ps1 | iex +prx onboard --interactive +prx daemon ``` -Default build (`cargo build`) includes `llm-router`. +The installer downloads a platform-specific binary from +[GitHub Releases](https://github.com/openprx/prx/releases), verifies its SHA-256 +checksum, and installs it without requiring Rust or Git. See the +[installation guide](docs/one-click-bootstrap.md) for exact versions, source +builds, supported platforms, upgrades, and removal. + +To build from a checkout with the pinned Rust 1.97.1 toolchain: -Or download pre-built binaries from [Releases](https://github.com/openprx/prx/releases). +```bash +git clone https://github.com/openprx/prx.git +cd prx +cargo build --release --locked --bin prx +./target/release/prx onboard --interactive +``` ## Binaries | Binary | Description | |--------|-------------| -| `openprx` | Main AI daemon — providers, channels, tools, evolution | +| `prx` | Main AI daemon and CLI — providers, channels, tools, evolution | | `prx-node` | Lightweight remote node agent — runs on managed devices | ## Architecture ``` - Channels (19) Tools (38+) Remote Nodes + Channels Tools Remote Nodes Signal · WA · TG · ... Shell · MCP · ... macOS · Pi · ... │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────┐ - │ openprx daemon │ + │ prx daemon │ │ Agent Loop · Gateway · CTE · Xin · Memory · Evo │ └──────────────────────┬──────────────────────────────┘ │ - Providers (9 LLMs) + Providers Anthropic · OpenAI · Google · ... ``` @@ -84,22 +99,22 @@ Or download pre-built binaries from [Releases](https://github.com/openprx/prx/re | Topic | Description | |-------|-------------| -| [Providers](docs/providers.md) | 9 LLM providers, fallback chains, token refresh | -| [Channels](docs/channels.md) | 19 messaging platforms, DM/group policies | -| [Tools](docs/tools.md) | 38 built-in tools, hooks system, webhooks | +| [Installation](docs/one-click-bootstrap.md) | Binary install, source build, upgrades, rollback | +| [Providers](docs/providers.md) | Provider catalog, fallback chains, token refresh | +| [Channels](docs/channels.md) | Messaging platforms, DM/group policies | +| [Tools](docs/tools.md) | Built-in tools, hooks system, webhooks | | [Remote Nodes](docs/remote-nodes.md) | `prx-node` agent, device pairing, JSON-RPC | | [Web Console](docs/web-console.md) | Browser-based management interface | | [Evolution](docs/evolution.md) | Self-improvement pipeline | | [Configuration](docs/configuration.md) | Config reference, workspace files, security | | [Router](docs/router.md) | LLM Router config, flow, safety boundaries | -| [Causal Tree Engine](docs/causal-tree.md) | CTE pipeline, branch prediction, rehearsal, scoring | | [WASM Plugins](docs/plugin-developer-guide.md) | Plugin developer guide (Rust/Python/JS/Go) | | [Host Function Reference](docs/host-function-reference.md) | WASM plugin host API reference | | [Plugin Runtime Lifecycle](docs/plugin-runtime-lifecycle.md) | Atomic generations, subscriber pumps, trust and hook bounds | ## Links -- [Documentation](https://docs.openprx.dev/en/prx/) — Full PRX documentation (10 languages) +- [Documentation](https://docs.openprx.dev/en/prx/) — Full PRX documentation - [Community](https://community.openprx.dev) — OpenPRX community forum - [OpenPRX](https://openprx.dev) — Project homepage diff --git a/docs/actions-source-policy.md b/docs/actions-source-policy.md new file mode 100644 index 00000000..5a623928 --- /dev/null +++ b/docs/actions-source-policy.md @@ -0,0 +1,17 @@ +# GitHub Actions Source Policy + +Workflow actions are part of PRX's build and release supply chain. + +- Prefer an immutable full commit SHA for third-party actions. +- Keep the human-readable upstream version in an adjacent comment. +- Use the repository-pinned Rust version for normal CI and release jobs. +- Keep nightly Rust limited to explicitly nightly-only tools such as Miri and + fuzzing. +- Install Cargo-based CI tools with `--locked` and an explicit version when the + tool is not already locked by this repository. +- Any new action source must be reviewed for permissions, runtime behavior, and + release impact before merge. + +The security audit installs `cargo-audit` with an explicit version and locked +dependencies. This prevents an unrelated tool dependency update from silently +raising the compiler requirement above PRX's pinned toolchain. diff --git a/docs/channels.md b/docs/channels.md index 4c8bd6f8..9dc0f43e 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -1,6 +1,7 @@ # Channels -OpenPRX supports 19 messaging channels. Each channel can be independently configured with DM/group policies. +PRX supports multiple messaging channels. Each channel can be independently +configured with DM/group policies. ## Supported Channels diff --git a/docs/configuration.md b/docs/configuration.md index 5846b5de..121e5b48 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -6,10 +6,10 @@ OpenPRX uses `~/.openprx/config.toml` as the main configuration file. ```bash # Interactive setup wizard -openprx onboard +prx onboard --interactive -# Or quick non-interactive setup -openprx onboard --quick +# Inspect the effective configuration +prx config show ``` ## Configuration Tree Transactions diff --git a/docs/evolution.md b/docs/evolution.md index 26b7f31f..7eda8b50 100644 --- a/docs/evolution.md +++ b/docs/evolution.md @@ -16,12 +16,13 @@ Record (realtime) → Analyze (daily) → Evolve (every 3 days) - **Evolution engines**: Memory evolution, prompt evolution, strategy evolution - **Safety**: Rollback capability, gate checks, shadow mode for first rounds - **Pipeline**: Scheduler, pipeline orchestration, annotation system -- **22 modules, ~9500 lines** of evolution infrastructure +- **Integrated runtime**: evolution state, history, configuration, and manual + trigger commands are exposed through the main CLI ## CLI ```bash -openprx evolution status # Show evolution state -openprx evolution trigger # Manually trigger evolution cycle -openprx evolution rollback # Rollback last evolution +prx evolution status # Show evolution state +prx evolution history # Show evolution history +prx evolution trigger # Manually trigger one evolution cycle ``` diff --git a/docs/one-click-bootstrap.md b/docs/one-click-bootstrap.md new file mode 100644 index 00000000..79912b50 --- /dev/null +++ b/docs/one-click-bootstrap.md @@ -0,0 +1,156 @@ +# Install PRX + +Last verified: **July 26, 2026** for PRX 0.8.20 and Rust 1.97.1. + +## Linux and macOS + +Install the latest release: + +```bash +curl -fsSL https://github.com/openprx/prx/releases/latest/download/install.sh | sh +``` + +The default destination is `~/.local/bin/prx`. If that directory is not +already in `PATH`, activate it in the current shell: + +```bash +export PATH="$HOME/.local/bin:$PATH" +``` + +Then configure PRX: + +```bash +prx onboard --interactive +``` + +The installer does not collect provider credentials. Onboarding is separate so +API keys are entered through PRX rather than embedded in a downloaded command. + +## Windows + +Run in PowerShell: + +```powershell +irm https://github.com/openprx/prx/releases/latest/download/install.ps1 | iex +prx onboard --interactive +``` + +The PowerShell installer adds its per-user installation directory to the user +`Path`. Open a new terminal if an existing shell does not see `prx`. + +## What the Installer Verifies + +The binary installers: + +1. Detect the operating system and CPU architecture. +2. Download the matching archive from the latest GitHub Release. +3. Download and validate the archive's SHA-256 checksum. +4. Extract only the `prx` executable. +5. Replace the destination atomically. +6. Run `prx --version` from the installed path. + +Installation fails closed when the checksum is missing, malformed, or does not +match. The installer does not silently fall back to a source build. + +## Supported Binary Targets + +| Operating system | Architecture | Release asset | +|---|---|---| +| Linux with glibc | x86_64 | `prx-linux-amd64.tar.gz` | +| Linux with glibc | ARM64 | `prx-linux-arm64.tar.gz` | +| macOS | Intel | `prx-macos-amd64.tar.gz` | +| macOS | Apple Silicon | `prx-macos-arm64.tar.gz` | +| Windows | x86_64 | `prx-windows-amd64.zip` | + +Alpine and other musl-based Linux systems must currently use a source build. +The installer reports unsupported systems explicitly instead of attempting to +run an incompatible glibc binary. + +## Exact Version, Custom Directory, and Rollback + +Install or roll back to an exact release: + +```bash +curl -fsSL https://github.com/openprx/prx/releases/latest/download/install.sh | + sh -s -- --version v0.8.20 +``` + +Choose a destination: + +```bash +curl -fsSL https://github.com/openprx/prx/releases/latest/download/install.sh | + sh -s -- --install-dir "$HOME/bin" +``` + +Re-running the installer replaces only the `prx` executable in the selected +directory. Configuration and workspace data under `~/.openprx/` are not +modified. + +Windows equivalents: + +```powershell +& ([scriptblock]::Create((irm https://github.com/openprx/prx/releases/latest/download/install.ps1))) -Version v0.8.20 +``` + +## Source Build + +The repository pins Rust 1.97.1. A source install requires Rust 1.97.1 or newer, +Cargo, Git, a C/C++ build toolchain, and platform development libraries. + +From a checkout: + +```bash +git clone https://github.com/openprx/prx.git +cd prx +cargo build --release --locked --bin prx +install -m 0755 target/release/prx "$HOME/.local/bin/prx" +``` + +The Unix installer also provides an explicit source mode: + +```bash +curl -fsSL https://github.com/openprx/prx/releases/latest/download/install.sh | + sh -s -- --source +``` + +## Daemon Service + +Installing the binary does not create or start a background service. After +onboarding, service setup remains explicit: + +```bash +prx service install +prx service start +prx service status +``` + +Apply a new binary or configuration with: + +```bash +prx service restart +``` + +## Remove PRX + +Stop and remove the service first: + +```bash +prx service stop +prx service uninstall +rm -f "$HOME/.local/bin/prx" +``` + +This intentionally leaves `~/.openprx/` in place. Remove that directory only +when its configuration, credentials, sessions, and workspace are no longer +needed. + +## Developer Bootstrap + +Repository contributors can use: + +```bash +./scripts/bootstrap.sh --help +``` + +That script supports source builds, dependency setup, and Docker onboarding. It +is not the user-facing one-click binary installer. diff --git a/docs/release-notes-0.8.20.md b/docs/release-notes-0.8.20.md new file mode 100644 index 00000000..6220f875 --- /dev/null +++ b/docs/release-notes-0.8.20.md @@ -0,0 +1,28 @@ +# PRX 0.8.20 Release Notes + +PRX 0.8.20 aligns the build, installation, release, and documentation surfaces. + +## Installation + +- Added a pre-built-first Unix installer at `install.sh`. +- Added a Windows PowerShell installer at `install.ps1`. +- Added exact-version installation and rollback support. +- Added fail-closed SHA-256 verification and atomic binary replacement. +- Kept onboarding and service creation explicit so installation does not handle + credentials or start background services unexpectedly. + +## Rust Toolchain + +- Raised the pinned and declared Rust version to 1.97.1. +- Aligned local builds, CI, security audit, release jobs, and Docker builds. +- Updated Rust 1.97 Clippy compatibility without changing runtime defaults. +- Preserved constant-time comparison and floating-point accumulation semantics + where mechanical lint suggestions would have changed behavior. + +## Release Integrity + +- Release tags must match the Cargo package version. +- Release asset names now match the installer platform mapping. +- Windows and Unix checksum sidecars use the same portable format. +- Release publication verifies asset completeness and runs the Linux binary + before creating the GitHub Release. diff --git a/docs/router.md b/docs/router.md index cbb887ab..95ba81ff 100644 --- a/docs/router.md +++ b/docs/router.md @@ -112,7 +112,8 @@ categories = ["conversation"] Operational note: - Ensure provider credentials and network are valid. -- Verify health with `openprx channel doctor` and your deployment health checks before enabling router in production. +- Verify health with `prx channel doctor` and your deployment health checks + before enabling router in production. ## Field Reference @@ -124,7 +125,6 @@ Operational note: - `gamma`: Elo weight - `delta`: cost penalty coefficient - `epsilon`: latency penalty coefficient -- `knn_min_records`: minimum history before semantic KNN affects routing - `knn_min_records`: minimum successful history records before KNN contributes - `knn_k`: nearest neighbors used for KNN voting - `models`: static candidate registry diff --git a/examples/custom_channel.rs b/examples/custom_channel.rs index 34bf42fe..a9b6c2cb 100644 --- a/examples/custom_channel.rs +++ b/examples/custom_channel.rs @@ -125,8 +125,7 @@ impl Channel for TelegramChannel { .get(self.api_url("getMe")) .send() .await - .map(|r| r.status().is_success()) - .unwrap_or(false) + .is_ok_and(|r| r.status().is_success()) } } diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 00000000..6c94d559 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,94 @@ +[CmdletBinding()] +param( + [string]$Version = "latest", + [string]$InstallDir = "", + [switch]$NoModifyPath +) + +$ErrorActionPreference = "Stop" +$Repository = "openprx/prx" + +if ([string]::IsNullOrWhiteSpace($InstallDir)) { + $localAppData = [Environment]::GetFolderPath("LocalApplicationData") + $InstallDir = Join-Path $localAppData "Programs\PRX" +} + +if ($Version -eq "latest") { + $releasePath = "latest/download" +} +elseif ($Version -match '^v?\d+\.\d+\.\d+(?:[-.][0-9A-Za-z.-]+)?$') { + if (-not $Version.StartsWith("v")) { + $Version = "v$Version" + } + $releasePath = "download/$Version" +} +else { + throw "Version must be 'latest' or a semantic version such as v0.8.20." +} + +$architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture +if ($architecture -ne [System.Runtime.InteropServices.Architecture]::X64) { + throw "Unsupported Windows architecture: $architecture. Current releases support Windows x64." +} + +$archiveName = "prx-windows-amd64.zip" +$baseUrl = "https://github.com/$Repository/releases/$releasePath" +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("prx-install-" + [guid]::NewGuid()) +$archivePath = Join-Path $tempDir $archiveName +$checksumPath = "$archivePath.sha256" +$extractDir = Join-Path $tempDir "extract" + +New-Item -ItemType Directory -Path $tempDir | Out-Null +try { + Write-Host "==> Downloading $archiveName" + Invoke-WebRequest -UseBasicParsing -Uri "$baseUrl/$archiveName" -OutFile $archivePath + Invoke-WebRequest -UseBasicParsing -Uri "$baseUrl/$archiveName.sha256" -OutFile $checksumPath + + $checksumContent = Get-Content -Raw $checksumPath + $checksumMatch = [regex]::Match($checksumContent, '(?i)\b[0-9a-f]{64}\b') + if (-not $checksumMatch.Success) { + throw "Release checksum is malformed." + } + $expected = $checksumMatch.Value.ToLowerInvariant() + $actual = (Get-FileHash -Algorithm SHA256 $archivePath).Hash.ToLowerInvariant() + if ($actual -ne $expected) { + throw "SHA-256 verification failed." + } + + Expand-Archive -Path $archivePath -DestinationPath $extractDir + $sourceBinary = Join-Path $extractDir "prx.exe" + if (-not (Test-Path -PathType Leaf $sourceBinary)) { + throw "Release archive does not contain prx.exe." + } + + New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + $destination = Join-Path $InstallDir "prx.exe" + $temporary = Join-Path $InstallDir (".prx-" + [guid]::NewGuid() + ".tmp") + Copy-Item $sourceBinary $temporary + Move-Item -Force $temporary $destination + + if (-not $NoModifyPath) { + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + $pathParts = @($userPath -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + if ($pathParts -notcontains $InstallDir) { + $newPath = (($pathParts + $InstallDir) -join ';') + [Environment]::SetEnvironmentVariable("Path", $newPath, "User") + } + if (($env:Path -split ';') -notcontains $InstallDir) { + $env:Path = "$InstallDir;$env:Path" + } + } + + Write-Host "==> Installed PRX to $destination" + & $destination --version + Write-Host "" + Write-Host "Next:" + Write-Host " prx onboard --interactive" + Write-Host "" + Write-Host "Optional daemon service:" + Write-Host " prx service install" + Write-Host " prx service start" +} +finally { + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir +} diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..e0cb07d4 --- /dev/null +++ b/install.sh @@ -0,0 +1,279 @@ +#!/bin/sh +set -eu + +REPOSITORY="openprx/prx" +REQUIRED_RUST_VERSION="1.97.1" +VERSION="latest" +INSTALL_DIR="${PRX_INSTALL_DIR:-${HOME}/.local/bin}" +SOURCE_BUILD="false" + +info() { + printf '==> %s\n' "$*" >&2 +} + +error() { + printf 'error: %s\n' "$*" >&2 +} + +usage() { + cat <<'EOF' +Install PRX from a verified GitHub Release binary. + +Usage: + install.sh [--version ] [--install-dir ] [--source] + +Options: + --version Install an exact release (default: latest) + --install-dir Destination directory (default: ~/.local/bin) + --source Build from source with Rust 1.97.1 or newer + -h, --help Show this help + +The default binary path does not require Rust, Cargo, Git, or sudo. +After installation, configure PRX with: + prx onboard --interactive +EOF +} + +need_value() { + if [ "$#" -lt 2 ] || [ -z "$2" ]; then + error "$1 requires a value" + exit 2 + fi +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --version) + need_value "$@" + VERSION="$2" + shift 2 + ;; + --install-dir) + need_value "$@" + INSTALL_DIR="$2" + shift 2 + ;; + --source) + SOURCE_BUILD="true" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + error "unknown option: $1" + usage >&2 + exit 2 + ;; + esac +done + +if [ "$VERSION" = "latest" ]; then + RELEASE_PATH="latest/download" +else + normalized_version="${VERSION#v}" + if ! printf '%s\n' "$normalized_version" | + grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$'; then + error "version must be 'latest' or a semantic version such as v0.8.20" + exit 2 + fi + VERSION="v$normalized_version" + RELEASE_PATH="download/$VERSION" +fi + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +version_at_least() { + awk -v current="$1" -v required="$2" 'BEGIN { + split(current, c, "."); + split(required, r, "."); + for (i = 1; i <= 3; i++) { + c[i] += 0; + r[i] += 0; + if (c[i] > r[i]) exit 0; + if (c[i] < r[i]) exit 1; + } + exit 0; + }' +} + +make_temp_dir() { + temp_root="${TMPDIR:-/tmp}" + mktemp -d "${temp_root%/}/prx-install.XXXXXX" +} + +install_atomically() { + source_binary="$1" + destination="$INSTALL_DIR/prx" + temporary="$INSTALL_DIR/.prx.tmp.$$" + + mkdir -p "$INSTALL_DIR" + cp "$source_binary" "$temporary" + chmod 0755 "$temporary" + mv -f "$temporary" "$destination" + printf '%s\n' "$destination" +} + +install_from_source() { + if ! command_exists git || ! command_exists cargo || ! command_exists rustc; then + error "--source requires git, cargo, and rustc" + error "install Rust ${REQUIRED_RUST_VERSION} or newer from https://rustup.rs/" + exit 1 + fi + + current_rust="$(rustc --version | awk '{print $2}')" + if ! version_at_least "$current_rust" "$REQUIRED_RUST_VERSION"; then + error "Rust ${REQUIRED_RUST_VERSION} or newer is required (found ${current_rust})" + exit 1 + fi + + temp_dir="$(make_temp_dir)" + trap 'rm -rf "$temp_dir"' EXIT HUP INT TERM + + if [ "$VERSION" = "latest" ]; then + info "Cloning the latest PRX source" + git clone --depth 1 "https://github.com/${REPOSITORY}.git" "$temp_dir/source" + else + info "Cloning PRX ${VERSION}" + git clone --depth 1 --branch "$VERSION" "https://github.com/${REPOSITORY}.git" "$temp_dir/source" + fi + + info "Building PRX with Rust ${current_rust}" + ( + cd "$temp_dir/source" + cargo build --release --locked --bin prx + ) + installed="$(install_atomically "$temp_dir/source/target/release/prx")" + trap - EXIT HUP INT TERM + rm -rf "$temp_dir" + printf '%s\n' "$installed" +} + +detect_asset_suffix() { + os="$(uname -s)" + arch="$(uname -m)" + + case "$os" in + Linux) + if [ -f /etc/alpine-release ] || (command_exists ldd && ldd --version 2>&1 | grep -qi musl); then + error "pre-built PRX binaries currently require glibc; use --source on musl/Alpine" + return 1 + fi + case "$arch" in + x86_64|amd64) printf '%s\n' "linux-amd64" ;; + aarch64|arm64) printf '%s\n' "linux-arm64" ;; + *) + error "unsupported Linux architecture: $arch" + return 1 + ;; + esac + ;; + Darwin) + case "$arch" in + x86_64|amd64) printf '%s\n' "macos-amd64" ;; + arm64|aarch64) printf '%s\n' "macos-arm64" ;; + *) + error "unsupported macOS architecture: $arch" + return 1 + ;; + esac + ;; + *) + error "unsupported operating system: $os" + error "Windows users should run install.ps1" + return 1 + ;; + esac +} + +download() { + url="$1" + output="$2" + curl --proto '=https' --tlsv1.2 -fsSL "$url" -o "$output" +} + +verify_sha256() { + archive="$1" + checksum_file="$2" + expected="$(awk 'NR == 1 {print tolower($1)}' "$checksum_file")" + + if ! printf '%s\n' "$expected" | grep -Eq '^[0-9a-f]{64}$'; then + error "release checksum is malformed" + return 1 + fi + + if command_exists sha256sum; then + actual="$(sha256sum "$archive" | awk '{print tolower($1)}')" + elif command_exists shasum; then + actual="$(shasum -a 256 "$archive" | awk '{print tolower($1)}')" + else + error "sha256sum or shasum is required to verify the release" + return 1 + fi + + if [ "$actual" != "$expected" ]; then + error "SHA-256 verification failed" + return 1 + fi +} + +install_prebuilt() { + if ! command_exists curl || ! command_exists tar; then + error "curl and tar are required for binary installation" + exit 1 + fi + + suffix="$(detect_asset_suffix)" + archive_name="prx-${suffix}.tar.gz" + base_url="https://github.com/${REPOSITORY}/releases/${RELEASE_PATH}" + temp_dir="$(make_temp_dir)" + trap 'rm -rf "$temp_dir"' EXIT HUP INT TERM + + info "Downloading ${archive_name}" + download "${base_url}/${archive_name}" "$temp_dir/$archive_name" + download "${base_url}/${archive_name}.sha256" "$temp_dir/$archive_name.sha256" + verify_sha256 "$temp_dir/$archive_name" "$temp_dir/$archive_name.sha256" + + mkdir "$temp_dir/extract" + tar -xzf "$temp_dir/$archive_name" -C "$temp_dir/extract" prx + if [ ! -f "$temp_dir/extract/prx" ]; then + error "release archive does not contain prx" + exit 1 + fi + + installed="$(install_atomically "$temp_dir/extract/prx")" + trap - EXIT HUP INT TERM + rm -rf "$temp_dir" + printf '%s\n' "$installed" +} + +umask 022 +if [ "$SOURCE_BUILD" = "true" ]; then + PRX_BINARY="$(install_from_source)" +else + PRX_BINARY="$(install_prebuilt)" +fi + +info "Installed PRX to ${PRX_BINARY}" +"$PRX_BINARY" --version + +case ":${PATH:-}:" in + *":${INSTALL_DIR}:"*) ;; + *) + printf '\n%s\n' "Add PRX to this shell's PATH:" + printf ' export PATH="%s:$PATH"\n' "$INSTALL_DIR" + ;; +esac + +cat <<'EOF' + +Next: + prx onboard --interactive + +Optional daemon service: + prx service install + prx service start +EOF diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 075062e5..99461977 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,4 @@ [toolchain] -channel = "1.93.0" +channel = "1.97.1" +components = ["clippy", "rustfmt"] +profile = "minimal" diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 94e13049..05aee7b8 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -15,14 +15,13 @@ error() { usage() { cat <<'USAGE' -PRX installer bootstrap engine +PRX developer/source bootstrap Usage: - ./prx_install.sh [options] - ./bootstrap.sh [options] # compatibility entrypoint + ./scripts/bootstrap.sh [options] Modes: - Default mode installs/builds PRX only (requires existing Rust toolchain). + Default mode builds and installs PRX from this checkout. Guided mode asks setup questions and configures options interactively. Optional bootstrap mode can also install system dependencies and Rust. @@ -46,19 +45,16 @@ Options: -h, --help Show help Examples: - ./prx_install.sh - ./prx_install.sh --guided - ./prx_install.sh --install-system-deps --install-rust - ./prx_install.sh --prefer-prebuilt - ./prx_install.sh --prebuilt-only - ./prx_install.sh --onboard --api-key "sk-..." --provider openrouter [--model "openrouter/auto"] - ./prx_install.sh --interactive-onboard + ./scripts/bootstrap.sh + ./scripts/bootstrap.sh --guided + ./scripts/bootstrap.sh --install-system-deps --install-rust + ./scripts/bootstrap.sh --prefer-prebuilt + ./scripts/bootstrap.sh --prebuilt-only + ./scripts/bootstrap.sh --onboard --api-key "sk-..." --provider openrouter [--model "openrouter/auto"] + ./scripts/bootstrap.sh --interactive-onboard - # Compatibility entrypoint: - ./bootstrap.sh --docker - - # Remote one-liner - curl -fsSL https://raw.githubusercontent.com/openprx/prx/main/scripts/bootstrap.sh | bash + # User-facing pre-built installer: + curl -fsSL https://github.com/openprx/prx/releases/latest/download/install.sh | sh Environment: PRX_DOCKER_DATA_DIR Host path for Docker config/workspace persistence @@ -157,26 +153,29 @@ get_available_disk_mb() { fi } -detect_release_target() { +detect_release_suffix() { local os arch os="$(uname -s)" arch="$(uname -m)" + if [[ "$os" == "Linux" ]] \ + && { [[ -f /etc/alpine-release ]] || { have_cmd ldd && ldd --version 2>&1 | grep -qi musl; }; }; then + warn "Pre-built PRX binaries currently require glibc; use the source-build path on musl/Alpine." + return 1 + fi + case "$os:$arch" in Linux:x86_64) - echo "x86_64-unknown-linux-gnu" + echo "linux-amd64" ;; Linux:aarch64|Linux:arm64) - echo "aarch64-unknown-linux-gnu" - ;; - Linux:armv7l|Linux:armv6l) - echo "armv7-unknown-linux-gnueabihf" + echo "linux-arm64" ;; Darwin:x86_64) - echo "x86_64-apple-darwin" + echo "macos-amd64" ;; Darwin:arm64|Darwin:aarch64) - echo "aarch64-apple-darwin" + echo "macos-arm64" ;; *) return 1 @@ -220,7 +219,7 @@ should_attempt_prebuilt_for_resources() { } install_prebuilt_binary() { - local target archive_url temp_dir archive_path extracted_bin install_dir + local suffix archive_url temp_dir archive_path extracted_bin install_dir if ! have_cmd curl; then warn "curl is required for pre-built binary installation." @@ -231,19 +230,19 @@ install_prebuilt_binary() { return 1 fi - target="$(detect_release_target || true)" - if [[ -z "$target" ]]; then + suffix="$(detect_release_suffix || true)" + if [[ -z "$suffix" ]]; then warn "No pre-built binary target mapping for $(uname -s)/$(uname -m)." return 1 fi - archive_url="https://github.com/openprx/prx/releases/latest/download/prx-${target}.tar.gz" + archive_url="https://github.com/openprx/prx/releases/latest/download/prx-${suffix}.tar.gz" local checksum_url="${archive_url}.sha256" temp_dir="$(mktemp -d -t prx-prebuilt-XXXXXX)" - archive_path="$temp_dir/prx-${target}.tar.gz" + archive_path="$temp_dir/prx-${suffix}.tar.gz" local checksum_path="${archive_path}.sha256" - info "Attempting pre-built binary install for target: $target" + info "Attempting pre-built binary install for target: $suffix" if ! curl -fsSL "$archive_url" -o "$archive_path"; then warn "Could not download release asset: $archive_url" rm -rf "$temp_dir" @@ -661,7 +660,7 @@ ensure_docker_ready() { error "docker is not installed." cat <<'MSG' >&2 Install Docker first, then re-run with: - ./prx_install.sh --docker + ./scripts/bootstrap.sh --docker MSG exit 1 fi @@ -710,9 +709,9 @@ run_docker_bootstrap() { Use either: --api-key "sk-..." or: - PRX_API_KEY="sk-..." ./prx_install.sh --docker + PRX_API_KEY="sk-..." ./scripts/bootstrap.sh --docker or run interactive: - ./prx_install.sh --docker --interactive-onboard + ./scripts/bootstrap.sh --docker --interactive-onboard MSG exit 1 fi @@ -906,10 +905,9 @@ cleanup() { } trap cleanup EXIT -# Support three launch modes: -# 1) ./bootstrap.sh from repo root -# 2) scripts/bootstrap.sh from repo -# 3) curl | bash (no local repo => temporary clone) +# Support two launch modes: +# 1) scripts/bootstrap.sh from a repository checkout +# 2) a copied script outside a checkout (temporary source clone) if [[ ! -f "$WORK_DIR/Cargo.toml" ]]; then if [[ -f "$(pwd)/Cargo.toml" ]]; then WORK_DIR="$(pwd)" @@ -963,8 +961,8 @@ DONE cat <<'DONE' Next steps: - ./prx_install.sh --docker --interactive-onboard - ./prx_install.sh --docker --api-key "sk-..." --provider openrouter + ./scripts/bootstrap.sh --docker --interactive-onboard + ./scripts/bootstrap.sh --docker --api-key "sk-..." --provider openrouter DONE exit 0 fi @@ -997,7 +995,7 @@ if [[ "$PREBUILT_INSTALLED" == false && ( "$SKIP_BUILD" == false || "$SKIP_INSTA cat <<'MSG' >&2 Install Rust first: https://rustup.rs/ or re-run with: - ./prx_install.sh --install-rust + ./scripts/bootstrap.sh --install-rust MSG exit 1 fi @@ -1042,9 +1040,9 @@ if [[ "$RUN_ONBOARD" == true ]]; then Use either: --api-key "sk-..." or: - PRX_API_KEY="sk-..." ./prx_install.sh --onboard + PRX_API_KEY="sk-..." ./scripts/bootstrap.sh --onboard or run interactive: - ./prx_install.sh --interactive-onboard + ./scripts/bootstrap.sh --interactive-onboard MSG exit 1 fi diff --git a/scripts/ci/installer_contract_gate.sh b/scripts/ci/installer_contract_gate.sh new file mode 100755 index 00000000..334fc768 --- /dev/null +++ b/scripts/ci/installer_contract_gate.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +installer="$repo_root/install.sh" +release_workflow="$repo_root/.github/workflows/release.yml" +temp_dir="$(mktemp -d -t prx-installer-contract-XXXXXX)" +trap 'rm -rf "$temp_dir"' EXIT + +fail() { + echo "installer contract failure: $*" >&2 + exit 1 +} + +test -x "$installer" || fail "install.sh must be executable" +sh -n "$installer" +pwsh -NoProfile -Command "& { [void][scriptblock]::Create((Get-Content -Raw '$repo_root/install.ps1')) }" \ + 2>/dev/null || { + if command -v pwsh >/dev/null 2>&1; then + fail "install.ps1 failed to parse" + fi + echo "pwsh unavailable; skipped PowerShell parse check" +} + +for suffix in linux-amd64 linux-arm64 macos-amd64 macos-arm64 windows-amd64; do + grep -q "prx-${suffix}" "$release_workflow" || fail "release workflow is missing prx-${suffix}" +done + +fixture_dir="$temp_dir/fixtures" +mock_bin="$temp_dir/mock-bin" +install_dir="$temp_dir/install/bin" +mkdir -p "$fixture_dir" "$mock_bin" "$temp_dir/archive" + +cat >"$temp_dir/archive/prx" <<'EOF' +#!/bin/sh +echo "prx 9.9.9-test" +EOF +chmod +x "$temp_dir/archive/prx" +tar -czf "$fixture_dir/prx-linux-amd64.tar.gz" -C "$temp_dir/archive" prx +sha256sum "$fixture_dir/prx-linux-amd64.tar.gz" >"$fixture_dir/prx-linux-amd64.tar.gz.sha256" + +cat >"$mock_bin/uname" <<'EOF' +#!/bin/sh +case "${1:-}" in + -s) echo "${PRX_TEST_OS:-Linux}" ;; + -m) echo "${PRX_TEST_ARCH:-x86_64}" ;; + *) echo "${PRX_TEST_OS:-Linux}" ;; +esac +EOF + +cat >"$mock_bin/ldd" <<'EOF' +#!/bin/sh +echo "${PRX_TEST_LDD:-ldd (GNU libc) 2.39}" +EOF + +cat >"$mock_bin/rustc" <<'EOF' +#!/bin/sh +echo "rustc 1.96.0 (test)" +EOF + +cat >"$mock_bin/curl" <<'EOF' +#!/bin/sh +set -eu +url="" +output="" +while [ "$#" -gt 0 ]; do + case "$1" in + -o) + output="$2" + shift 2 + ;; + http*) + url="$1" + shift + ;; + *) + shift + ;; + esac +done +printf '%s\n' "$url" >>"$PRX_TEST_URL_LOG" +cp "$PRX_FIXTURE_DIR/$(basename "$url")" "$output" +EOF +chmod +x "$mock_bin/uname" "$mock_bin/ldd" "$mock_bin/rustc" "$mock_bin/curl" + +export PRX_FIXTURE_DIR="$fixture_dir" +export PRX_TEST_URL_LOG="$temp_dir/urls.log" +test_path="$mock_bin:$PATH" + +HOME="$temp_dir/home" PATH="$test_path" "$installer" \ + --version v9.9.9 \ + --install-dir "$install_dir" >"$temp_dir/install.out" + +test -x "$install_dir/prx" || fail "installer did not create an executable" +grep -q "prx 9.9.9-test" "$temp_dir/install.out" || fail "installed binary was not verified" +grep -q "/releases/download/v9.9.9/prx-linux-amd64.tar.gz" "$temp_dir/urls.log" \ + || fail "exact-version asset URL is incorrect" + +printf '%064d %s\n' 0 "prx-linux-amd64.tar.gz" >"$fixture_dir/prx-linux-amd64.tar.gz.sha256" +if HOME="$temp_dir/home" PATH="$test_path" "$installer" \ + --version v9.9.9 \ + --install-dir "$temp_dir/tampered/bin" >"$temp_dir/tampered.out" 2>&1; then + fail "installer accepted a bad checksum" +fi +test ! -e "$temp_dir/tampered/bin/prx" || fail "tampered archive was installed" + +if HOME="$temp_dir/home" PATH="$test_path" PRX_TEST_ARCH="mips64" "$installer" \ + --version v9.9.9 \ + --install-dir "$temp_dir/unsupported/bin" >"$temp_dir/unsupported.out" 2>&1; then + fail "installer accepted an unsupported architecture" +fi +grep -q "unsupported Linux architecture" "$temp_dir/unsupported.out" \ + || fail "unsupported-architecture error is not actionable" + +if HOME="$temp_dir/home" PATH="$test_path" PRX_TEST_LDD="musl libc" "$installer" \ + --version v9.9.9 \ + --install-dir "$temp_dir/musl/bin" >"$temp_dir/musl.out" 2>&1; then + fail "installer accepted a glibc binary on musl" +fi +grep -q "require glibc" "$temp_dir/musl.out" \ + || fail "musl error is not actionable" + +if HOME="$temp_dir/home" PATH="$test_path" "$installer" \ + --source \ + --install-dir "$temp_dir/source/bin" >"$temp_dir/source.out" 2>&1; then + fail "source installer accepted Rust older than 1.97.1" +fi +grep -q "Rust 1.97.1 or newer is required" "$temp_dir/source.out" \ + || fail "source Rust version error is not actionable" + +echo "Installer contract gate passed." diff --git a/scripts/ci/rust_toolchain_sync_gate.sh b/scripts/ci/rust_toolchain_sync_gate.sh new file mode 100755 index 00000000..20b20dec --- /dev/null +++ b/scripts/ci/rust_toolchain_sync_gate.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$repo_root" + +toolchain_version="$(awk -F'"' '/^channel = / { print $2; exit }' rust-toolchain.toml)" +cargo_version="$(awk -F'"' ' + /^\[package\]$/ { in_package = 1; next } + /^\[/ { in_package = 0 } + in_package && /^rust-version = / { print $2; exit } +' Cargo.toml)" +docker_version="$(sed -nE 's/^FROM rust:([0-9.]+)-slim@.*/\1/p' Dockerfile | head -n 1)" + +test -n "$toolchain_version" +test "$cargo_version" = "$toolchain_version" +test "$docker_version" = "$toolchain_version" + +mapfile -t workflow_versions < <( + rg -o 'toolchain: [0-9]+\.[0-9]+\.[0-9]+' .github/workflows \ + | awk '{print $2}' \ + | sort -u +) +test "${#workflow_versions[@]}" -eq 1 +test "${workflow_versions[0]}" = "$toolchain_version" + +default_reusable_version="$( + awk -F'"' ' + /default: "[0-9]+\.[0-9]+\.[0-9]+"/ { print $2; exit } + ' .github/workflows/test-rust-build.yml +)" +test "$default_reusable_version" = "$toolchain_version" + +echo "Rust toolchain contract is synchronized at ${toolchain_version}." diff --git a/scripts/release/cut_release_tag.sh b/scripts/release/cut_release_tag.sh index aacf1e0d..fed0603a 100755 --- a/scripts/release/cut_release_tag.sh +++ b/scripts/release/cut_release_tag.sh @@ -9,9 +9,10 @@ Create an annotated release tag from the current checkout. Requirements: - tag must match vX.Y.Z (optional suffix like -rc.1) -- working tree must be clean +- working tree must be clean, including untracked files - HEAD must match origin/main - tag must not already exist locally or on origin +- tag must match the Cargo package version Options: --push Push the tag to origin after creating it @@ -44,11 +45,23 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 1 fi -if ! git diff --quiet || ! git diff --cached --quiet; then +if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then echo "error: working tree is not clean; commit or stash changes first" >&2 exit 1 fi +PACKAGE_VERSION="$( + awk -F'"' ' + /^\[package\]$/ { in_package = 1; next } + /^\[/ { in_package = 0 } + in_package && /^version = / { print $2; exit } + ' Cargo.toml +)" +if [[ "$TAG" != "v$PACKAGE_VERSION" ]]; then + echo "error: tag $TAG does not match Cargo package version $PACKAGE_VERSION" >&2 + exit 1 +fi + echo "Fetching origin/main and tags..." git fetch --quiet origin main --tags @@ -77,7 +90,7 @@ echo "Created annotated tag: $TAG" if [[ "$PUSH_TAG" == "true" ]]; then git push origin "$TAG" echo "Pushed tag to origin: $TAG" - echo "GitHub release pipeline will run via .github/workflows/pub-release.yml" + echo "GitHub release pipeline will run via .github/workflows/release.yml" else echo "Next step: git push origin $TAG" fi diff --git a/src/agent/classifier.rs b/src/agent/classifier.rs index cb3f35e1..02d1d934 100644 --- a/src/agent/classifier.rs +++ b/src/agent/classifier.rs @@ -37,7 +37,7 @@ pub fn classify(config: &QueryClassificationConfig, message: &str) -> Option = config.rules.iter().collect(); - rules.sort_by(|a, b| b.priority.cmp(&a.priority)); + rules.sort_by_key(|rule| std::cmp::Reverse(rule.priority)); for rule in rules { // Length constraints @@ -70,7 +70,7 @@ pub fn classify(config: &QueryClassificationConfig, message: &str) -> Option ClassifyResult { let lower = message.to_lowercase(); let mut rules: Vec<_> = config.rules.iter().collect(); - rules.sort_by(|a, b| b.priority.cmp(&a.priority)); + rules.sort_by_key(|rule| std::cmp::Reverse(rule.priority)); for rule in rules { let keyword_hit = rule diff --git a/src/causal_tree/engine.rs b/src/causal_tree/engine.rs index 91901947..9bbcb5fa 100644 --- a/src/causal_tree/engine.rs +++ b/src/causal_tree/engine.rs @@ -211,7 +211,7 @@ impl CausalTreeEngine { self.check_timeout(policy, pipeline_start)?; let scored: Vec<(CausalBranch, f32, Option)> = branches .into_iter() - .zip(artifacts.into_iter()) + .zip(artifacts) .filter_map(|(branch, artifact)| { let score = self.scorer.score(state, &branch, artifact.as_ref(), &self.config)?; Some((branch, score, artifact)) diff --git a/src/channels/discord.rs b/src/channels/discord.rs index 05df08af..cd998d8c 100644 --- a/src/channels/discord.rs +++ b/src/channels/discord.rs @@ -517,7 +517,7 @@ impl Channel for DiscordChannel { let handle = tokio::spawn(async move { let url = format!("https://discord.com/api/v10/channels/{channel_id}/typing"); // Auto-expire after 60 seconds (safety net if stop_typing is never called) - let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(60); + let deadline = tokio::time::Instant::now() + std::time::Duration::from_mins(1); loop { if tokio::time::Instant::now() >= deadline { break; diff --git a/src/channels/email_channel.rs b/src/channels/email_channel.rs index ce44ec1b..d59e070d 100644 --- a/src/channels/email_channel.rs +++ b/src/channels/email_channel.rs @@ -345,7 +345,7 @@ impl EmailChannel { /// Main IDLE-based listen loop with automatic reconnection async fn listen_with_idle(&self, tx: mpsc::Sender) -> Result<()> { let mut backoff = Duration::from_secs(1); - let max_backoff = Duration::from_secs(60); + let max_backoff = Duration::from_mins(1); loop { match self.run_idle_session(&tx).await { diff --git a/src/channels/irc.rs b/src/channels/irc.rs index ef725f07..dab93d07 100644 --- a/src/channels/irc.rs +++ b/src/channels/irc.rs @@ -10,7 +10,7 @@ use tokio_rustls::rustls; /// Read timeout for IRC — if no data arrives within this duration, the /// connection is considered dead. IRC servers typically PING every 60-120s. -const READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300); +const READ_TIMEOUT: std::time::Duration = std::time::Duration::from_mins(5); /// Monotonic counter to ensure unique message IDs under burst traffic. static MSG_SEQ: AtomicU64 = AtomicU64::new(0); diff --git a/src/channels/lark.rs b/src/channels/lark.rs index 5c4ed346..86603174 100644 --- a/src/channels/lark.rs +++ b/src/channels/lark.rs @@ -125,11 +125,11 @@ struct LarkMessage { /// Heartbeat timeout for WS connection — must be larger than ping_interval (default 120 s). /// If no binary frame (pong or event) is received within this window, reconnect. -const WS_HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(300); +const WS_HEARTBEAT_TIMEOUT: Duration = Duration::from_mins(5); /// Refresh tenant token this many seconds before the announced expiry. -const LARK_TOKEN_REFRESH_SKEW: Duration = Duration::from_secs(120); +const LARK_TOKEN_REFRESH_SKEW: Duration = Duration::from_mins(2); /// Fallback tenant token TTL when `expire`/`expires_in` is absent. -const LARK_DEFAULT_TOKEN_TTL: Duration = Duration::from_secs(7200); +const LARK_DEFAULT_TOKEN_TTL: Duration = Duration::from_hours(2); /// Feishu/Lark API business code for expired/invalid tenant access token. const LARK_INVALID_ACCESS_TOKEN_CODE: i64 = 99_991_663; @@ -386,7 +386,7 @@ impl LarkChannel { break; } // GC stale fragments > 5 min - let cutoff = Instant::now().checked_sub(Duration::from_secs(300)).unwrap_or_else(Instant::now); + let cutoff = Instant::now().checked_sub(Duration::from_mins(5)).unwrap_or_else(Instant::now); frag_cache.retain(|_, (_, ts)| *ts > cutoff); } @@ -505,7 +505,7 @@ impl LarkChannel { let now = Instant::now(); let mut seen = self.ws_seen_ids.write().await; // GC - seen.retain(|_, t| now.duration_since(*t) < Duration::from_secs(30 * 60)); + seen.retain(|_, t| now.duration_since(*t) < Duration::from_mins(30)); if seen.contains_key(&lark_msg.message_id) { tracing::debug!("Lark WS: dup {}", lark_msg.message_id); continue; @@ -1063,7 +1063,7 @@ mod tests { let regular = next_token_refresh_deadline(now, 7200); let short_ttl = next_token_refresh_deadline(now, 60); - assert_eq!(regular.duration_since(now), Duration::from_secs(7080)); + assert_eq!(regular.duration_since(now), Duration::from_mins(118)); assert_eq!(short_ttl.duration_since(now), Duration::from_secs(1)); } diff --git a/src/channels/linq.rs b/src/channels/linq.rs index 864d9215..b0039338 100644 --- a/src/channels/linq.rs +++ b/src/channels/linq.rs @@ -295,7 +295,7 @@ impl Channel for LinqChannel { // Keep the task alive — it will be cancelled when the channel shuts down loop { - tokio::time::sleep(std::time::Duration::from_secs(3600)).await; + tokio::time::sleep(std::time::Duration::from_hours(1)).await; } } diff --git a/src/channels/mod.rs b/src/channels/mod.rs index e50a2ec3..504e3b77 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -3898,7 +3898,7 @@ async fn run_message_dispatch_loop( HashMap::::new(), )); let task_sequence = Arc::new(AtomicU64::new(1)); - let mut health_heartbeat = tokio::time::interval(std::time::Duration::from_secs(60)); + let mut health_heartbeat = tokio::time::interval(std::time::Duration::from_mins(1)); health_heartbeat.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop { @@ -11076,11 +11076,11 @@ After"#; ); assert_eq!( channel_supervisor_sleep_duration(CHANNEL_CIRCUIT_BREAKER_FAILURES, 16, 60), - Duration::from_secs(300) + Duration::from_mins(5) ); assert_eq!( channel_supervisor_sleep_duration(CHANNEL_CIRCUIT_BREAKER_FAILURES + 10, 60, 60), - Duration::from_secs(300) + Duration::from_mins(5) ); } diff --git a/src/channels/nextcloud_talk.rs b/src/channels/nextcloud_talk.rs index 6f27da67..10ff8fc2 100644 --- a/src/channels/nextcloud_talk.rs +++ b/src/channels/nextcloud_talk.rs @@ -221,7 +221,7 @@ impl Channel for NextcloudTalkChannel { // Keep task alive; incoming events are handled by the gateway webhook handler. loop { - tokio::time::sleep(std::time::Duration::from_secs(3600)).await; + tokio::time::sleep(std::time::Duration::from_hours(1)).await; } } diff --git a/src/channels/signal.rs b/src/channels/signal.rs index dfe852e0..b24c8486 100644 --- a/src/channels/signal.rs +++ b/src/channels/signal.rs @@ -1647,7 +1647,7 @@ impl SignalChannel { let response = self .http_client() .get(&url) - .timeout(Duration::from_secs(60)) + .timeout(Duration::from_mins(1)) .send() .await .ok()?; diff --git a/src/channels/slack.rs b/src/channels/slack.rs index 988d99b9..694bac9f 100644 --- a/src/channels/slack.rs +++ b/src/channels/slack.rs @@ -509,11 +509,11 @@ mod tests { ); assert_eq!( SlackChannel::poll_backoff_duration(7), - std::time::Duration::from_secs(60) + std::time::Duration::from_mins(1) ); assert_eq!( SlackChannel::poll_backoff_duration(99), - std::time::Duration::from_secs(60) + std::time::Duration::from_mins(1) ); } } diff --git a/src/channels/telegram.rs b/src/channels/telegram.rs index e2018de2..01d0e6c9 100644 --- a/src/channels/telegram.rs +++ b/src/channels/telegram.rs @@ -1913,7 +1913,7 @@ mod tests { { let mut guard = ch.typing_handle.lock(); *guard = Some(tokio::spawn(async { - tokio::time::sleep(Duration::from_secs(60)).await; + tokio::time::sleep(Duration::from_mins(1)).await; })); } @@ -1932,7 +1932,7 @@ mod tests { { let mut guard = ch.typing_handle.lock(); *guard = Some(tokio::spawn(async { - tokio::time::sleep(Duration::from_secs(60)).await; + tokio::time::sleep(Duration::from_mins(1)).await; })); } diff --git a/src/channels/wacli.rs b/src/channels/wacli.rs index bef18979..154a8f52 100644 --- a/src/channels/wacli.rs +++ b/src/channels/wacli.rs @@ -75,7 +75,7 @@ const DEFAULT_MAX_IMAGE_BYTES: usize = 5 * 1024 * 1024; /// Replay/idempotency cache TTL. Messages whose key was seen within this window /// are treated as duplicates and dropped (200, not forwarded). -const REPLAY_TTL: Duration = Duration::from_secs(300); +const REPLAY_TTL: Duration = Duration::from_mins(5); /// Maximum number of keys retained in the replay cache. const REPLAY_MAX_KEYS: usize = 10_000; diff --git a/src/channels/whatsapp.rs b/src/channels/whatsapp.rs index bb70c915..5d434706 100644 --- a/src/channels/whatsapp.rs +++ b/src/channels/whatsapp.rs @@ -213,7 +213,7 @@ impl Channel for WhatsAppChannel { // Keep the task alive — it will be cancelled when the channel shuts down loop { - tokio::time::sleep(std::time::Duration::from_secs(3600)).await; + tokio::time::sleep(std::time::Duration::from_hours(1)).await; } } diff --git a/src/chat/dispatcher.rs b/src/chat/dispatcher.rs index 8074d055..5b3981e2 100644 --- a/src/chat/dispatcher.rs +++ b/src/chat/dispatcher.rs @@ -5865,7 +5865,7 @@ mod real_mode_tests { // 收第一条 Action:默认 stream 实现发一个 error chunk(delta=error message, // is_final=true),转换成 StreamChunkReceived (因 delta 非空). - let action = tokio::time::timeout(Duration::from_millis(1000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(1), action_rx.recv()) .await .expect("action within 1s") .expect("action received"); @@ -5878,7 +5878,7 @@ mod real_mode_tests { } // T3-3-fixB B5: 第二条是 RecordAssistantTurn(在 StreamCompleted 前发出). - let action = tokio::time::timeout(Duration::from_millis(1000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(1), action_rx.recv()) .await .expect("RecordAssistantTurn within 1s") .expect("RecordAssistantTurn received"); @@ -5888,7 +5888,7 @@ mod real_mode_tests { } // 第三条:is_final=true 进入 break,发送 StreamCompleted. - let action = tokio::time::timeout(Duration::from_millis(1000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(1), action_rx.recv()) .await .expect("completion within 1s") .expect("completion received"); @@ -7544,7 +7544,7 @@ mod real_mode_tests { let mut saw_completion = false; let mut final_text_seen = String::new(); for _ in 0..16 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver should respond within 2s per action") .expect("action must arrive"); @@ -7746,7 +7746,7 @@ mod real_mode_tests { // Drain until completion so the spawned driver task has finished the tool. for _ in 0..16 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver should respond within 2s per action") .expect("action must arrive"); @@ -7864,7 +7864,7 @@ mod real_mode_tests { let mut got_failed = false; for _ in 0..32 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver should respond per action within 2s") .expect("action must arrive"); @@ -8013,7 +8013,7 @@ mod real_mode_tests { let mut got_failed = false; let mut failed_err = String::new(); for _ in 0..32 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver should respond per action within 2s") .expect("action must arrive"); @@ -8622,7 +8622,7 @@ mod real_mode_tests { let mut saw_tool_finished = false; let mut saw_completion = false; for _ in 0..32 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver action within 2s") .expect("must arrive"); @@ -8761,7 +8761,7 @@ mod real_mode_tests { .await; for _ in 0..32 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver action within 2s") .expect("must arrive"); @@ -8883,7 +8883,7 @@ mod real_mode_tests { let mut saw_compacted = false; let mut saw_completion = false; for _ in 0..16 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver action within 2s") .expect("must arrive"); @@ -9020,7 +9020,7 @@ mod real_mode_tests { let mut saw_feedback = false; let mut saw_context_update = false; for _ in 0..16 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver action within 2s") .expect("must arrive"); @@ -9261,7 +9261,7 @@ mod real_mode_tests { let mut saw_completion = false; for _ in 0..8 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver action within 2s") .expect("driver action should arrive"); @@ -9404,7 +9404,7 @@ mod real_mode_tests { let mut saw_summary_patch = false; let mut saw_completion = false; for _ in 0..16 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver action within 2s") .expect("driver action should arrive"); @@ -10195,7 +10195,7 @@ mod real_mode_tests { let mut saw_summary_patch = false; let mut saw_completion = false; for _ in 0..16 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver action within 2s") .expect("must arrive"); @@ -10337,7 +10337,7 @@ mod real_mode_tests { let mut saw_local_guard_patch = false; let mut saw_completion = false; for _ in 0..16 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver action within 2s") .expect("must arrive"); @@ -10487,7 +10487,7 @@ mod real_mode_tests { let mut saw_feedback = false; let mut saw_context_update = false; for _ in 0..16 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("driver action within 2s") .expect("must arrive"); @@ -10626,7 +10626,7 @@ mod real_mode_tests { let mut saw_failed = false; for _ in 0..16 { - let action = tokio::time::timeout(Duration::from_millis(2000), action_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(2), action_rx.recv()) .await .expect("action within 2s") .expect("must arrive"); @@ -10801,7 +10801,7 @@ mod real_mode_tests { let mut saw_tool_finished = false; let mut saw_completion = false; for _ in 0..32 { - let action = tokio::time::timeout(Duration::from_millis(3000), sink_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(3), sink_rx.recv()) .await .expect("action within 3s") .expect("must arrive"); @@ -11032,7 +11032,7 @@ mod real_mode_tests { let mut saw_tool_finished_rejected = false; let mut saw_completion = false; for _ in 0..32 { - let action = tokio::time::timeout(Duration::from_millis(3000), sink_rx.recv()) + let action = tokio::time::timeout(Duration::from_secs(3), sink_rx.recv()) .await .expect("action within 3s") .expect("must arrive"); diff --git a/src/chat/mod.rs b/src/chat/mod.rs index b524ceed..329d8f08 100644 --- a/src/chat/mod.rs +++ b/src/chat/mod.rs @@ -6999,6 +6999,7 @@ Retry with a compatible model: /provider {new_provider} " // - Timeout: sleep 2s, retry once let mut context_overflow_retries = 0usize; let mut timeout_retries = 0usize; + #[cfg(feature = "terminal-tui")] let mut history_len_before_tools; // S2-A refinement: split the coarse `Failed` variant so the Redux @@ -7727,7 +7728,10 @@ Retry with a compatible model: /provider {new_provider} " ); let turn_outcome = loop { - history_len_before_tools = history.len(); + #[cfg(feature = "terminal-tui")] + { + history_len_before_tools = history.len(); + } let result = tokio::time::timeout( timeout_budget, @@ -12402,7 +12406,7 @@ fn run_tui_unified_loop( // 150 ms only while an on-screen animation is active. Idle mode uses a long // poll so a completed/empty TUI does not keep a fixed redraw/tick cadence. let active_animation_poll = Duration::from_millis(150); - let idle_poll = Duration::from_millis(1_000); + let idle_poll = Duration::from_secs(1); loop { if shutdown.is_cancelled() { diff --git a/src/chat/sessions/approval.rs b/src/chat/sessions/approval.rs index 21831098..e1ecacff 100644 --- a/src/chat/sessions/approval.rs +++ b/src/chat/sessions/approval.rs @@ -45,7 +45,7 @@ use crate::tools::sessions_spawn::{SubAgentRun, SubAgentStatus}; /// decision arrives within this window the resolver wakes itself with the safe /// default (deny) so a forgotten suspension cannot pin a concurrency slot /// forever. -pub const DEFAULT_APPROVAL_TIMEOUT: Duration = Duration::from_secs(300); +pub const DEFAULT_APPROVAL_TIMEOUT: Duration = Duration::from_mins(5); /// Shared registry of pending approval waiters, keyed by run id. /// @@ -477,7 +477,7 @@ mod tests { "rc", Arc::clone(&runs), pending.clone(), - Duration::from_secs(300), // long: only cancellation can wake us + Duration::from_mins(5), // long: only cancellation can wake us ); let req = request(); let token = CancellationToken::new(); @@ -519,7 +519,7 @@ mod tests { // stale sender in `PendingApprovals` (the leak this guards against). let runs = Arc::new(RwLock::new(vec![make_run("ra")])); let pending = PendingApprovals::new(); - let (r, mut events) = resolver("ra", Arc::clone(&runs), pending.clone(), Duration::from_secs(300)); + let (r, mut events) = resolver("ra", Arc::clone(&runs), pending.clone(), Duration::from_mins(5)); let req = request(); let handle = tokio::spawn(async move { r.resolve(&req, "sessions_spawn").await }); diff --git a/src/chat/sessions/pty.rs b/src/chat/sessions/pty.rs index 01d77dc9..cf5ff3a7 100644 --- a/src/chat/sessions/pty.rs +++ b/src/chat/sessions/pty.rs @@ -905,7 +905,7 @@ impl PtyShellSession { /// Maximum time [`reap_reader`] will wait for the drain-reader thread to finish /// before giving up and detaching it (v3b-a blocker-2: a truly bounded join). - const REAP_JOIN_DEADLINE: std::time::Duration = std::time::Duration::from_millis(1000); + const REAP_JOIN_DEADLINE: std::time::Duration = std::time::Duration::from_secs(1); /// Tear down the persistent drain reader: request it to stop, drop the writer /// (so the slave observes EOF), and **bounded**-join the reader thread. @@ -2180,7 +2180,7 @@ mod tests { assert!( captured.contains("line-200"), "drain reader did not capture detached output — child would block: {:?}", - &captured.get(captured.len().saturating_sub(80)..) + captured.get(captured.len().saturating_sub(80)..) ); session.kill().await.expect("test: cleanup kill"); diff --git a/src/chat/tui.rs b/src/chat/tui.rs index d34b88c2..b6cfa243 100644 --- a/src/chat/tui.rs +++ b/src/chat/tui.rs @@ -2470,11 +2470,9 @@ impl TuiInput { let mut new_cursor = (li, line.len()); // Any remaining parts become new lines below the current one. - let mut insert_at = li + 1; - for part in parts { + for (insert_at, part) in (li + 1..).zip(parts) { self.lines.insert(insert_at, part.to_string()); new_cursor = (insert_at, self.lines.get(insert_at).map_or(0, String::len)); - insert_at += 1; } // Append the original suffix to whatever ended up as the diff --git a/src/config/schema.rs b/src/config/schema.rs index 9ec83fcf..472e4da9 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -5878,14 +5878,7 @@ impl Config { .bind .parse::() .with_context(|| format!("invalid webhook.bind socket address: {}", self.webhook.bind))?; - if self - .webhook - .token - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .is_none() - { + if self.webhook.token.as_deref().map(str::trim).is_none_or(str::is_empty) { anyhow::bail!("webhook.token must not be empty"); } let webhook_backend = if self.storage.provider.config.provider.trim().is_empty() { diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 477bc8fc..ed4fd90c 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -1040,7 +1040,7 @@ async fn run_fitness_worker( ) -> Result<()> { let interval_hours = config.self_system.fitness_interval_hours.max(1); let mut interval = tokio::time::interval(Duration::from_secs(interval_hours.saturating_mul(3600))); - let mut health_heartbeat = tokio::time::interval(Duration::from_secs(60)); + let mut health_heartbeat = tokio::time::interval(Duration::from_mins(1)); crate::health::mark_component_ok("self_system_fitness"); wait_for_active_generation(&manager, generation_id).await; @@ -1076,7 +1076,7 @@ async fn run_evolution_scheduler_worker( ) -> Result<()> { let (mut scheduler, interval_hours) = build_evolution_scheduler(&config).await?; let mut interval = tokio::time::interval(Duration::from_secs(interval_hours.max(1).saturating_mul(3600))); - let mut health_heartbeat = tokio::time::interval(Duration::from_secs(60)); + let mut health_heartbeat = tokio::time::interval(Duration::from_mins(1)); crate::health::mark_component_ok("evolution_scheduler"); wait_for_active_generation(&manager, generation_id).await; diff --git a/src/gateway/api/sessions.rs b/src/gateway/api/sessions.rs index f827226b..a3c0e806 100644 --- a/src/gateway/api/sessions.rs +++ b/src/gateway/api/sessions.rs @@ -1172,7 +1172,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 4b356cb9..569a3ecf 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -1128,7 +1128,7 @@ pub async fn run_gateway( "gateway", "gateway", true, - Duration::from_secs(60), + Duration::from_mins(1), crate::health::ComponentState::Starting, ); // ── Security: refuse public bind without tunnel or explicit opt-in ── @@ -3064,7 +3064,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -3181,7 +3181,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -3237,7 +3237,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -3287,7 +3287,7 @@ mod tests { #[test] fn rate_limiter_sweep_removes_stale_entries() { - let limiter = SlidingWindowRateLimiter::new(10, Duration::from_secs(60), 100); + let limiter = SlidingWindowRateLimiter::new(10, Duration::from_mins(1), 100); // Add entries for multiple IPs assert!(limiter.allow("ip-1")); assert!(limiter.allow("ip-2")); @@ -3321,7 +3321,7 @@ mod tests { #[test] fn rate_limiter_zero_limit_always_allows() { - let limiter = SlidingWindowRateLimiter::new(0, Duration::from_secs(60), 10); + let limiter = SlidingWindowRateLimiter::new(0, Duration::from_mins(1), 10); for _ in 0..100 { assert!(limiter.allow("any-key")); } @@ -3366,7 +3366,7 @@ mod tests { #[test] fn rate_limiter_bounded_cardinality_evicts_least_active_key() { - let limiter = SlidingWindowRateLimiter::new(5, Duration::from_secs(60), 2); + let limiter = SlidingWindowRateLimiter::new(5, Duration::from_mins(1), 2); // ip-1 gets 2 requests, ip-2 gets 1 — ip-2 is least active assert!(limiter.allow("ip-1")); assert!(limiter.allow("ip-1")); @@ -3382,7 +3382,7 @@ mod tests { #[test] fn idempotency_store_never_evicts_live_or_unexpired_entries() { - let store = Arc::new(IdempotencyStore::new(Duration::from_secs(300), 2)); + let store = Arc::new(IdempotencyStore::new(Duration::from_mins(5), 2)); let first = acquire_idempotency_claim(&store, "k1", test_fingerprint(1)); let second = acquire_idempotency_claim(&store, "k2", test_fingerprint(2)); @@ -3788,7 +3788,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -4011,7 +4011,7 @@ mod tests { let provider_impl = Arc::new(BlockFirstProvider::default()); let provider: Arc = provider_impl.clone(); let mut state = webhook_test_state(provider); - state.idempotency_store = Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1)); + state.idempotency_store = Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1)); let mut first_headers = HeaderMap::new(); first_headers.insert("X-Idempotency-Key", HeaderValue::from_static("first-key")); @@ -4163,7 +4163,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -4305,7 +4305,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -4375,7 +4375,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -4441,7 +4441,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -4494,7 +4494,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -4549,7 +4549,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -4604,7 +4604,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -4657,7 +4657,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -4718,7 +4718,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -4779,7 +4779,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -5020,7 +5020,7 @@ mod tests { #[test] fn idempotency_store_allows_different_keys() { - let store = Arc::new(IdempotencyStore::new(Duration::from_secs(60), 100)); + let store = Arc::new(IdempotencyStore::new(Duration::from_mins(1), 100)); let claims = ["key-a", "key-b", "key-c", "key-d"] .into_iter() .enumerate() @@ -5032,7 +5032,7 @@ mod tests { #[test] fn idempotency_store_max_keys_clamped_to_one() { - let store = Arc::new(IdempotencyStore::new(Duration::from_secs(60), 0)); + let store = Arc::new(IdempotencyStore::new(Duration::from_mins(1), 0)); let _claim = acquire_idempotency_claim(&store, "only-key", test_fingerprint(1)); assert!(matches!( store.claim("second-key".to_string(), test_fingerprint(2)), @@ -5042,7 +5042,7 @@ mod tests { #[test] fn idempotency_store_same_key_different_body_conflicts() { - let store = Arc::new(IdempotencyStore::new(Duration::from_secs(300), 100)); + let store = Arc::new(IdempotencyStore::new(Duration::from_mins(5), 100)); let _claim = acquire_idempotency_claim(&store, "rapid", test_fingerprint(1)); assert!(matches!( store.claim("rapid".to_string(), test_fingerprint(2)), @@ -5082,7 +5082,7 @@ mod tests { #[test] fn idempotency_store_stale_generation_cannot_finish_new_attempt() { - let store = Arc::new(IdempotencyStore::new(Duration::from_secs(60), 1)); + let store = Arc::new(IdempotencyStore::new(Duration::from_mins(1), 1)); let first = acquire_idempotency_claim(&store, "key", test_fingerprint(1)); let first_generation = first.generation; assert!(first.fail(true)); @@ -5103,7 +5103,7 @@ mod tests { #[test] fn idempotency_store_payload_budget_is_released_after_failure() { - let store = Arc::new(IdempotencyStore::new(Duration::from_secs(60), 100)); + let store = Arc::new(IdempotencyStore::new(Duration::from_mins(1), 100)); let mut claims = (0..(IDEMPOTENCY_REPLAY_BUDGET_BYTES / IDEMPOTENCY_MAX_REPLAY_BYTES)) .map(|index| acquire_idempotency_claim(&store, &format!("key-{index}"), test_fingerprint(index as u8))) .collect::>(); @@ -5118,7 +5118,7 @@ mod tests { #[test] fn idempotency_store_oversize_success_keeps_non_reexecuting_tombstone() { - let store = Arc::new(IdempotencyStore::new(Duration::from_secs(60), 1)); + let store = Arc::new(IdempotencyStore::new(Duration::from_mins(1), 1)); let claim = acquire_idempotency_claim(&store, "oversize", test_fingerprint(1)); let replay = IdempotencyReplay { response_id: Uuid::new_v4(), @@ -5168,7 +5168,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, @@ -5231,7 +5231,7 @@ mod tests { pairing: Arc::new(PairingGuard::new(false, &[])), trust_forwarded_headers: false, rate_limiter: Arc::new(GatewayRateLimiter::new(100, 100, 100, 100)), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, diff --git a/src/health/mod.rs b/src/health/mod.rs index eac339d4..6679d80e 100644 --- a/src/health/mod.rs +++ b/src/health/mod.rs @@ -6,7 +6,7 @@ use std::sync::OnceLock; use std::time::{Duration, Instant}; use tokio::sync::Notify; -const DEFAULT_FRESHNESS_TTL: Duration = Duration::from_secs(300); +const DEFAULT_FRESHNESS_TTL: Duration = Duration::from_mins(5); const MAX_PUBLIC_ERROR_CHARS: usize = 200; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] @@ -470,7 +470,7 @@ mod tests { &component, "second-owner", false, - Duration::from_secs(60), + Duration::from_mins(1), ComponentState::Starting, ); diff --git a/src/main.rs b/src/main.rs index 56ca617d..5144d4b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2354,16 +2354,15 @@ async fn handle_auth_command(auth_command: AuthCommands, config: &Config) -> Res println!(); println!("Waiting for callback at http://localhost:1455/auth/callback ..."); - let code = match auth::openai_oauth::receive_loopback_code(&pkce.state, std::time::Duration::from_secs(180)) - .await - { - Ok(code) => code, - Err(e) => { - println!("Callback capture failed: {e}"); - println!("Run `prx auth paste-redirect --provider openai-codex --profile {profile}`"); - return Ok(()); - } - }; + let code = + match auth::openai_oauth::receive_loopback_code(&pkce.state, std::time::Duration::from_mins(3)).await { + Ok(code) => code, + Err(e) => { + println!("Callback capture failed: {e}"); + println!("Run `prx auth paste-redirect --provider openai-codex --profile {profile}`"); + return Ok(()); + } + }; let token_set = auth::openai_oauth::exchange_code_for_tokens(&client, &code, &pkce).await?; let account_id = extract_openai_account_id_for_profile(&token_set.access_token); diff --git a/src/media/artifact.rs b/src/media/artifact.rs index 228f4465..f08ce509 100644 --- a/src/media/artifact.rs +++ b/src/media/artifact.rs @@ -14,7 +14,7 @@ const MAX_REMOTE_REDIRECTS: usize = 5; const MAX_URL_BYTES: usize = 2048; const MAX_MANAGED_ARTIFACTS: usize = 256; const MAX_MANAGED_BYTES: u64 = 512 * 1024 * 1024; -const ARTIFACT_TTL: Duration = Duration::from_secs(60 * 60); +const ARTIFACT_TTL: Duration = Duration::from_hours(1); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ArtifactSourceKind { diff --git a/src/media/mod.rs b/src/media/mod.rs index 576c974f..c1c1e697 100644 --- a/src/media/mod.rs +++ b/src/media/mod.rs @@ -12,7 +12,7 @@ use crate::config::MediaConfig; pub mod artifact; pub use artifact::{ArtifactError, LoadedArtifact, ManagedArtifact, MediaArtifactOwner}; -const AUDIO_TIMEOUT: Duration = Duration::from_secs(60); +const AUDIO_TIMEOUT: Duration = Duration::from_mins(1); const VIDEO_COMMAND_TIMEOUT: Duration = Duration::from_secs(30); const MAX_COMMAND_OUTPUT_BYTES: usize = 1024 * 1024; const MAX_FRAME_BYTES: usize = 5 * 1024 * 1024; diff --git a/src/memory/vector.rs b/src/memory/vector.rs index 6fd02fd2..bf0f30a5 100644 --- a/src/memory/vector.rs +++ b/src/memory/vector.rs @@ -1,6 +1,7 @@ // Vector operations — cosine similarity, normalization, hybrid merge. /// Cosine similarity between two vectors. Returns 0.0–1.0. +#[allow(clippy::suboptimal_flops)] // Preserve the established floating-point accumulation order. pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 { if a.len() != b.len() || a.is_empty() { return 0.0; diff --git a/src/nodes/client.rs b/src/nodes/client.rs index 3ace8860..59af8344 100644 --- a/src/nodes/client.rs +++ b/src/nodes/client.rs @@ -14,7 +14,7 @@ use std::time::{Duration, Instant}; use tokio::sync::Mutex; const FAILURE_THRESHOLD: u8 = 3; -const UNHEALTHY_COOLDOWN: Duration = Duration::from_secs(60); +const UNHEALTHY_COOLDOWN: Duration = Duration::from_mins(1); static PROCESS_NODE_MANAGERS: LazyLock>>> = LazyLock::new(|| RwLock::new(HashMap::new())); diff --git a/src/providers/compatible.rs b/src/providers/compatible.rs index 406245b9..8d668243 100644 --- a/src/providers/compatible.rs +++ b/src/providers/compatible.rs @@ -59,7 +59,7 @@ impl OpenAiCompatibleProvider { fn http_client_builder(&self) -> reqwest::ClientBuilder { let builder = Client::builder() - .timeout(std::time::Duration::from_secs(120)) + .timeout(std::time::Duration::from_mins(2)) .connect_timeout(std::time::Duration::from_secs(10)) .user_agent("OpenPRX/0.1"); let builder = if self.requires_http1_only() { diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 245e0177..e8e6ba43 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -1135,11 +1135,7 @@ pub fn scrub_secret_patterns(input: &str) -> String { for prefix in PREFIXES { let mut search_from = 0; - loop { - let Some(rel) = scrubbed[search_from..].find(prefix) else { - break; - }; - + while let Some(rel) = scrubbed[search_from..].find(prefix) { let start = search_from + rel; let content_start = start + prefix.len(); let end = token_end(&scrubbed, content_start); diff --git a/src/providers/openai_codex.rs b/src/providers/openai_codex.rs index 2c074deb..be032d2d 100644 --- a/src/providers/openai_codex.rs +++ b/src/providers/openai_codex.rs @@ -118,7 +118,7 @@ impl OpenAiCodexProvider { auth, auth_profile_override: options.auth_profile_override.clone(), client: Client::builder() - .timeout(std::time::Duration::from_secs(120)) + .timeout(std::time::Duration::from_mins(2)) .connect_timeout(std::time::Duration::from_secs(10)) .build() .unwrap_or_else(|_| Client::new()), @@ -484,11 +484,7 @@ fn parse_sse_text(body: &str) -> anyhow::Result> { Ok(()) }; - loop { - let Some(idx) = buffer.find("\n\n") else { - break; - }; - + while let Some(idx) = buffer.find("\n\n") { let chunk = buffer[..idx].to_string(); buffer = buffer[idx + 2..].to_string(); process_chunk(&chunk)?; diff --git a/src/providers/traits.rs b/src/providers/traits.rs index 1634ff83..636139a4 100644 --- a/src/providers/traits.rs +++ b/src/providers/traits.rs @@ -1333,7 +1333,7 @@ mod tests { aggregated.push_str(delta); } } - assert_eq!(aggregated, chunk_final.args, "Streaming 累积应等于 Completed 的 args",); + assert_eq!(aggregated, chunk_final.args, "Streaming 累积应等于 Completed 的 args"); assert_eq!(chunk1.id, chunk_final.id, "tool id 必须贯穿全 chunk 序列"); assert_eq!(chunk1.name, chunk_final.name, "tool name 必须贯穿全 chunk 序列"); assert_eq!( diff --git a/src/runtime/shell_process.rs b/src/runtime/shell_process.rs index 905e783b..39d48658 100644 --- a/src/runtime/shell_process.rs +++ b/src/runtime/shell_process.rs @@ -729,7 +729,7 @@ mod tests { .execute(ShellProcessRequest { command: &command, workspace_dir: &workspace, - timeout: Duration::from_secs(60), + timeout: Duration::from_mins(1), cancellation: None, }) .await diff --git a/src/security/pairing.rs b/src/security/pairing.rs index d2cba962..a04b9309 100644 --- a/src/security/pairing.rs +++ b/src/security/pairing.rs @@ -244,6 +244,7 @@ fn is_token_hash(value: &str) -> bool { /// /// Does not short-circuit on length mismatch — always iterates over the /// longer input to avoid leaking length information via timing. +#[allow(clippy::needless_bitwise_bool)] // Both comparisons must execute for the constant-time contract. pub fn constant_time_eq(a: &str, b: &str) -> bool { let a = a.as_bytes(); let b = b.as_bytes(); diff --git a/src/security/policy.rs b/src/security/policy.rs index 58c94400..3894362a 100644 --- a/src/security/policy.rs +++ b/src/security/policy.rs @@ -1127,7 +1127,7 @@ impl ActionTracker { pub fn record(&self) -> usize { let mut actions = self.actions.lock(); let cutoff = Instant::now() - .checked_sub(std::time::Duration::from_secs(3600)) + .checked_sub(std::time::Duration::from_hours(1)) .unwrap_or_else(Instant::now); actions.retain(|t| *t > cutoff); actions.push(Instant::now()); @@ -1138,7 +1138,7 @@ impl ActionTracker { pub fn count(&self) -> usize { let mut actions = self.actions.lock(); let cutoff = Instant::now() - .checked_sub(std::time::Duration::from_secs(3600)) + .checked_sub(std::time::Duration::from_hours(1)) .unwrap_or_else(Instant::now); actions.retain(|t| *t > cutoff); actions.len() @@ -1420,10 +1420,8 @@ fn contains_unquoted_single_ampersand(command: &str) -> bool { match ch { '\'' => quote = QuoteState::Single, '"' => quote = QuoteState::Double, - '&' => { - if chars.next_if_eq(&'&').is_none() { - return true; - } + '&' if chars.next_if_eq(&'&').is_none() => { + return true; } _ => {} } diff --git a/src/self_system/evolution/analyzer.rs b/src/self_system/evolution/analyzer.rs index ac6c51aa..bce499d9 100644 --- a/src/self_system/evolution/analyzer.rs +++ b/src/self_system/evolution/analyzer.rs @@ -510,7 +510,7 @@ fn extract_noise_memories(memory: &[MemoryAccessLog]) -> Vec evidence_ids, }) .collect::>(); - result.sort_by(|a, b| b.load_count.cmp(&a.load_count)); + result.sort_by_key(|item| std::cmp::Reverse(item.load_count)); result } @@ -589,7 +589,7 @@ fn extract_user_correction_clusters(decisions: &[DecisionLog]) -> Vec>(); - out.sort_by(|a, b| b.count.cmp(&a.count)); + out.sort_by_key(|item| std::cmp::Reverse(item.count)); out } diff --git a/src/skills/mod.rs b/src/skills/mod.rs index d1a5549e..3d165054 100644 --- a/src/skills/mod.rs +++ b/src/skills/mod.rs @@ -181,21 +181,18 @@ pub async fn load_skills_with_embeddings( let hydration_lock = { let mut locks = SKILL_HYDRATION_LOCKS.lock(); let existing = locks.get(&namespace).cloned(); - existing.map_or_else( - || { - if locks.len() >= MAX_HYDRATION_LOCKS { - locks.retain(|_, lock| Arc::strong_count(lock) > 1); - } - if locks.len() >= MAX_HYDRATION_LOCKS { - Arc::clone(&SKILL_HYDRATION_OVERFLOW_LOCK) - } else { - let lock = Arc::new(tokio::sync::Mutex::new(())); - locks.insert(namespace.clone(), Arc::clone(&lock)); - lock - } - }, - std::convert::identity, - ) + existing.unwrap_or_else(|| { + if locks.len() >= MAX_HYDRATION_LOCKS { + locks.retain(|_, lock| Arc::strong_count(lock) > 1); + } + if locks.len() >= MAX_HYDRATION_LOCKS { + Arc::clone(&SKILL_HYDRATION_OVERFLOW_LOCK) + } else { + let lock = Arc::new(tokio::sync::Mutex::new(())); + locks.insert(namespace.clone(), Arc::clone(&lock)); + lock + } + }) }; let _hydration_guard = hydration_lock.lock().await; let mut pending = Vec::new(); @@ -265,7 +262,7 @@ pub async fn hydrate_skill_embeddings( let descriptions: Vec<&str> = pending.iter().map(|(_, description)| description.as_str()).collect(); let embeddings = embedder.embed(&descriptions).await?; - for ((idx, _), embedding) in pending.into_iter().zip(embeddings.into_iter()) { + for ((idx, _), embedding) in pending.into_iter().zip(embeddings) { // SAFETY: idx was derived from skills.iter().enumerate(), so it is always valid #[allow(clippy::indexing_slicing)] { diff --git a/src/tools/delegate.rs b/src/tools/delegate.rs index 38dab057..35888e3f 100644 --- a/src/tools/delegate.rs +++ b/src/tools/delegate.rs @@ -729,7 +729,7 @@ impl Tool for DelegateTool { let tool_result = ToolResult { success: false, output: String::new(), - error: Some(format!("Agent '{agent_name}' failed: {e}",)), + error: Some(format!("Agent '{agent_name}' failed: {e}")), }; if let Some(fabric) = memory_fabric.as_ref() { let provider_outcome = crate::llm::route_decision::ProviderExecutionOutcome::failed_for_decision( diff --git a/src/tools/sessions_read_model.rs b/src/tools/sessions_read_model.rs index a34d6102..f81a595d 100644 --- a/src/tools/sessions_read_model.rs +++ b/src/tools/sessions_read_model.rs @@ -85,7 +85,7 @@ pub(crate) async fn recover_run_history( .into_iter() .filter(|event| event.run_id.as_deref() == Some(run_id) || event.parent_run_id.as_deref() == Some(run_id)) .collect::>(); - messages.sort_by(|a, b| a.id.cmp(&b.id)); + messages.sort_by_key(|a| a.id); if messages.len() > limit { let start = messages.len().saturating_sub(limit); messages = messages.split_off(start); diff --git a/src/tools/sessions_spawn.rs b/src/tools/sessions_spawn.rs index 2258007a..840cea85 100644 --- a/src/tools/sessions_spawn.rs +++ b/src/tools/sessions_spawn.rs @@ -6453,7 +6453,7 @@ mod tests { .await .expect("descendant pid should be published"); // SAFETY: signal 0 only probes the test-owned descendant PID. - assert!((unsafe { libc::kill(descendant_pid, 0) }) == 0); + assert_eq!((unsafe { libc::kill(descendant_pid, 0) }), 0); let control = ProcessRunControl::new(); let request = { diff --git a/tests/config_hotreload_authz_e2e.rs b/tests/config_hotreload_authz_e2e.rs index a2f1ad80..3c2ee5c1 100644 --- a/tests/config_hotreload_authz_e2e.rs +++ b/tests/config_hotreload_authz_e2e.rs @@ -231,7 +231,7 @@ fn watcher_shared_config_gateway_authz_e2e() { // // `spawn_blocking` uses a thread pool; scheduling can take a few hundred ms. // 2 s gives ample headroom for CI environments. - tokio::time::sleep(Duration::from_millis(2000)).await; + tokio::time::sleep(Duration::from_secs(2)).await; // ── Phase 4: trigger watcher — overwrite config.toml with ReadOnly ─────── // diff --git a/tests/int_agent_gateway.rs b/tests/int_agent_gateway.rs index cdbf3780..9977a08d 100644 --- a/tests/int_agent_gateway.rs +++ b/tests/int_agent_gateway.rs @@ -122,7 +122,7 @@ impl Provider for HangingProvider { let call_idx = self.call_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); if call_idx >= self.hang_on_call { // Hang for a very long time (simulates timeout) - tokio::time::sleep(Duration::from_secs(3600)).await; + tokio::time::sleep(Duration::from_hours(1)).await; anyhow::bail!("provider timeout (should not reach here)"); } let mut guard = self.responses.lock().expect("test: lock hanging responses"); @@ -346,7 +346,7 @@ fn build_test_app_state(overrides: TestAppStateOverrides) -> AppState { rate_limiter: overrides .rate_limiter .unwrap_or_else(|| Arc::new(GatewayRateLimiter::new(100, 100, 100, 100))), - idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)), + idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_mins(5), 1000)), whatsapp: None, signal: None, whatsapp_app_secret: None, diff --git a/tests/int_p1_cross_module.rs b/tests/int_p1_cross_module.rs index 41f7933b..543792d3 100644 --- a/tests/int_p1_cross_module.rs +++ b/tests/int_p1_cross_module.rs @@ -562,13 +562,13 @@ async fn int_gs_06_idempotency_store_creation_bounded() { use std::time::Duration; // Creating a store with max_keys=1 should work (min clamped to 1) - let _store = IdempotencyStore::new(Duration::from_secs(300), 1); + let _store = IdempotencyStore::new(Duration::from_mins(5), 1); // Creating a store with max_keys=10000 should work - let _store_large = IdempotencyStore::new(Duration::from_secs(300), 10_000); + let _store_large = IdempotencyStore::new(Duration::from_mins(5), 10_000); // Creating a store with max_keys=0 should be clamped to 1 (no panic) - let _store_zero = IdempotencyStore::new(Duration::from_secs(300), 0); + let _store_zero = IdempotencyStore::new(Duration::from_mins(5), 0); } // ═══════════════════════════════════════════════════════════════════════════════ From 911c86918b35f1aa8654ff045855cbfe33848cdf Mon Sep 17 00:00:00 2001 From: g1e2x87 Date: Sun, 26 Jul 2026 05:17:37 -0400 Subject: [PATCH 2/2] fix: remove ripgrep dependency from toolchain gate --- scripts/ci/rust_toolchain_sync_gate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/rust_toolchain_sync_gate.sh b/scripts/ci/rust_toolchain_sync_gate.sh index 20b20dec..fbf4aa4c 100755 --- a/scripts/ci/rust_toolchain_sync_gate.sh +++ b/scripts/ci/rust_toolchain_sync_gate.sh @@ -17,7 +17,7 @@ test "$cargo_version" = "$toolchain_version" test "$docker_version" = "$toolchain_version" mapfile -t workflow_versions < <( - rg -o 'toolchain: [0-9]+\.[0-9]+\.[0-9]+' .github/workflows \ + grep -rhoE 'toolchain: [0-9]+\.[0-9]+\.[0-9]+' .github/workflows \ | awk '{print $2}' \ | sort -u )