diff --git a/.github/workflows/nightly-deep-ci.yml b/.github/workflows/nightly-deep-ci.yml index ab457ac0b9a..47e89bf3152 100644 --- a/.github/workflows/nightly-deep-ci.yml +++ b/.github/workflows/nightly-deep-ci.yml @@ -12,6 +12,14 @@ name: Nightly Deep CI # - Windows compile matrix # - benchmark compilation # +# The Reborn binary suites are reused the same way: +# - .github/workflows/reborn-tests.yml: per-crate tests across the Reborn +# crate families plus the partitioned root parity tests (with +# event_name == schedule, scope detection short-circuits to full scope) +# - .github/workflows/reborn-e2e.yml: deterministic Rust contract gate +# (architecture/runtimes/substrates) plus gateway and WebUI v2 smoke +# (workflow_call bypasses the PR path filters, so the full gate runs) +# # Docker is intentionally excluded here because QA/release Docker images are # owned by the separate Docker pipeline and need release-team alignment. # Full browser E2E is scheduled separately by .github/workflows/e2e.yml. @@ -36,6 +44,18 @@ jobs: ref: ${{ github.sha }} include_docker: false + reborn-tests: + name: Reborn Tests + uses: ./.github/workflows/reborn-tests.yml + with: + ref: ${{ github.sha }} + + reborn-e2e: + name: Reborn E2E + uses: ./.github/workflows/reborn-e2e.yml + with: + ref: ${{ github.sha }} + nightly-alert: name: Nightly Deep CI Alert runs-on: ubuntu-latest @@ -44,7 +64,7 @@ jobs: actions: read contents: read issues: write - needs: [deterministic-deep-tests] + needs: [deterministic-deep-tests, reborn-tests, reborn-e2e] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: @@ -55,5 +75,5 @@ jobs: REPO: ${{ github.repository }} ALERT_WORKFLOW_NAME: Nightly Deep CI ALERT_ISSUE_TITLE: Nightly Deep CI failed - ALERT_RESULT: ${{ needs.deterministic-deep-tests.result }} + ALERT_RESULT: ${{ (needs.deterministic-deep-tests.result == 'success' && needs.reborn-tests.result == 'success' && needs.reborn-e2e.result == 'success') && 'success' || 'failure' }} run: .github/scripts/nightly-alert-issue.sh diff --git a/.github/workflows/reborn-integration.yml b/.github/workflows/reborn-integration.yml deleted file mode 100644 index b9540f66d17..00000000000 --- a/.github/workflows/reborn-integration.yml +++ /dev/null @@ -1,227 +0,0 @@ -name: Reborn Integration Binary Crate Tests - -on: - workflow_dispatch: - inputs: - ref: - description: Commit SHA or ref to test - required: false - type: string - pull_request: - branches: - - reborn-integration - merge_group: - branches: - - reborn-integration - types: - - checks_requested - push: - branches: - - reborn-integration - -permissions: - contents: read - -concurrency: - group: reborn-integration-${{ github.event_name }}-${{ github.head_ref || github.ref }} - cancel-in-progress: true - -env: - # Tests must never touch the real OS keychain (macOS Keychain auth dialog / - # Linux Secret Service). Guarded by src/secrets/keychain.rs::os_keychain_suppressed: - # cfg!(test) covers unit tests; this covers integration/e2e that link the - # non-cfg(test) library. - IRONCLAW_DISABLE_OS_KEYCHAIN: "1" - # PR #5086 measured mold linking the heaviest Reborn build in parallel with - # no OOM, so keep Cargo's default parallelism. If runner OOMs recur, restore - # the previous one-job Cargo build serialization as the one-line fallback. - RUSTFLAGS: "-C linker=clang -C link-arg=--ld-path=/usr/bin/mold" - CARGO_PROFILE_DEV_DEBUG: 0 - CARGO_PROFILE_TEST_DEBUG: 0 - -jobs: - package-matrix: - name: Discover Reborn package crates - runs-on: ubuntu-latest - outputs: - packages: ${{ steps.packages.outputs.packages }} - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - ref: ${{ inputs.ref || github.sha }} - persist-credentials: false - - - name: Install Rust - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - - id: packages - name: Build package matrix from Reborn crate families - run: | - packages="$( - cargo metadata --no-deps --format-version 1 \ - | jq -c ' - [ - .packages[] - | select( - (.name | startswith("ironclaw_reborn")) - or (.name | startswith("ironclaw_product")) - or (.name == "ironclaw_architecture") - or (.name == "ironclaw_slack_v2_adapter") - or (.name == "ironclaw_telegram_v2_adapter") - or (.name == "ironclaw_wasm_product_adapters") - or (.name | startswith("ironclaw_webui_v2")) - ) - | .name - ] - | unique - ' - )" - - if [ -z "${packages}" ] || [ "${packages}" = "[]" ]; then - echo "No Reborn workspace crates discovered" >&2 - exit 1 - fi - - echo "packages=${packages}" >> "$GITHUB_OUTPUT" - echo "Testing Reborn package crates:" - printf '%s\n' "${packages}" | jq -r '.[] | "- " + .' - - crate-tests: - name: Test ${{ matrix.package }} - needs: package-matrix - runs-on: ubuntu-latest - timeout-minutes: 30 - env: - ANTHROPIC_API_KEY: "" - LLM_BACKEND: "" - LLM_USE_CODEX_AUTH: "false" - OLLAMA_BASE_URL: "" - OPENAI_API_KEY: "" - strategy: - fail-fast: true - matrix: - package: ${{ fromJSON(needs.package-matrix.outputs.packages) }} - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - ref: ${{ inputs.ref || github.sha }} - persist-credentials: false - - - name: Free disk space - run: | - df -h / - sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup /usr/local/share/boost /opt/hostedtoolcache/CodeQL || true - docker system prune -af || true - df -h / - - - name: Install Rust - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - - name: Install mold and clang - run: sudo apt-get update && sudo apt-get install -y clang mold - - - name: Restore Rust cache - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - with: - key: reborn-integration-${{ matrix.package }} - # Keep saves to protected-branch and merge_group runs so the ~10 GB - # repo cache LRU is seeded by shared states, not arbitrary PR branches. - save-if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/reborn-integration') || github.event_name == 'merge_group' }} - - - name: Run crate tests - run: | - feature_flags="$(scripts/ci/package-feature-flags.sh "${{ matrix.package }}")" - - # shellcheck disable=SC2086 # feature_flags intentionally expands to zero or more Cargo args. - timeout --signal=INT --kill-after=30s 28m \ - cargo test -p "${{ matrix.package }}" ${feature_flags} --all-targets -- --nocapture - - root-reborn-parity-tests: - name: Reborn root tests (${{ matrix.partition }}) - runs-on: ubuntu-latest - timeout-minutes: 45 - env: - ANTHROPIC_API_KEY: "" - LLM_BACKEND: "" - LLM_USE_CODEX_AUTH: "false" - OLLAMA_BASE_URL: "" - OPENAI_API_KEY: "" - REBORN_ROOT_TEST_PARTITIONS: 4 - REBORN_ROOT_TEST_PARTITION: ${{ matrix.partition }} - REBORN_ROOT_TEST_TIMEOUT: 28m - strategy: - fail-fast: true - matrix: - partition: [0, 1, 2, 3] - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - ref: ${{ inputs.ref || github.sha }} - persist-credentials: false - - - name: Free disk space - run: | - df -h / - sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup /usr/local/share/boost /opt/hostedtoolcache/CodeQL || true - docker system prune -af || true - df -h / - - - name: Install Rust - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - - name: Install mold and clang - run: sudo apt-get update && sudo apt-get install -y clang mold - - - name: Verify mold linker is active - run: | - clang_output="$( - printf 'int main(void) { return 0; }\n' \ - | clang --ld-path=/usr/bin/mold -Wl,--version -x c - -o /tmp/clang-mold-check 2>&1 - )" - printf '%s\n' "$clang_output" - grep -qi mold <<< "$clang_output" - - printf 'fn main() { println!("mold-link-ok"); }\n' > /tmp/mold-link-check.rs - rustc /tmp/mold-link-check.rs \ - -C linker=clang \ - -C link-arg=--ld-path=/usr/bin/mold \ - -o /tmp/mold-link-check - test "$(/tmp/mold-link-check)" = "mold-link-ok" - - - name: Restore Rust cache - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - with: - key: reborn-integration-root-${{ matrix.partition }} - # Keep saves to protected-branch and merge_group runs so the ~10 GB - # repo cache LRU is seeded by shared states, not arbitrary PR branches. - save-if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/reborn-integration') || github.event_name == 'merge_group' }} - - - name: Run Reborn root tests - run: scripts/ci/run-reborn-root-partition.sh - - reborn-integration-crate-tests: - name: Reborn Integration Binary Crate Tests - runs-on: ubuntu-latest - if: always() - needs: - - package-matrix - - crate-tests - - root-reborn-parity-tests - steps: - - name: Check matrix result - run: | - if [[ "${{ needs.package-matrix.result }}" != "success" ]]; then - echo "Reborn package matrix discovery failed" - exit 1 - fi - if [[ "${{ needs.crate-tests.result }}" != "success" ]]; then - echo "One or more Reborn binary crate tests failed" - exit 1 - fi - if [[ "${{ needs.root-reborn-parity-tests.result }}" != "success" ]]; then - echo "One or more Reborn root tests failed" - exit 1 - fi diff --git a/crates/ironclaw_auth/tests/auth_product_contract/oauth_helpers_contract.rs b/crates/ironclaw_auth/tests/auth_product_contract/oauth_helpers_contract.rs index 161f74eb5e8..510a2000d81 100644 --- a/crates/ironclaw_auth/tests/auth_product_contract/oauth_helpers_contract.rs +++ b/crates/ironclaw_auth/tests/auth_product_contract/oauth_helpers_contract.rs @@ -248,7 +248,11 @@ fn google_callback_state_round_trips_through_validated_encoded_state() { #[test] fn google_callback_state_rejects_unapproved_requested_scopes() { - let invalid_scope = ProviderScope::new("https://www.googleapis.com/auth/drive").unwrap(); + // `gmail.insert` is a real, sensitive Gmail scope deliberately kept out of + // the approved GSuite set (`is_allowed_google_scope`). The approved set grew + // to include the Drive/Docs/Sheets/Slides scopes in #4326, so this guards the + // boundary with a scope that is still outside it. + let invalid_scope = ProviderScope::new("https://www.googleapis.com/auth/gmail.insert").unwrap(); assert_invalid_request(GoogleOAuthCallbackState::new( AuthFlowId::new(), diff --git a/crates/ironclaw_loop_support/src/capability_port/provider_validation.rs b/crates/ironclaw_loop_support/src/capability_port/provider_validation.rs index 28e9cfd47eb..87308df5fde 100644 --- a/crates/ironclaw_loop_support/src/capability_port/provider_validation.rs +++ b/crates/ironclaw_loop_support/src/capability_port/provider_validation.rs @@ -91,13 +91,19 @@ mod tests { #[test] fn provider_tool_call_validation_rejects_sensitive_metadata() { + // Arguments carrying a real secret-like token are rejected by the + // entropy-based leak scan, which is the canonical guard after #5001 + // dropped the crude bare-word substring markers. let mut call = provider_tool_call(); let api_key = format!("sk-proj-{}", "a".repeat(24)); call.arguments = serde_json::json!({"password": api_key}); assert!(validate_provider_tool_call(&call).is_err()); + // The same leak scan runs over model-emitted reasoning, so a leaked + // secret-like token there is rejected even though bare words like + // "traceback" are now intentionally allowed (#5001, PinchBench bucket D). let mut call = provider_tool_call(); - call.reasoning = Some("provider error included traceback".to_string()); + call.reasoning = Some(format!("provider error leaked sk-proj-{}", "b".repeat(24))); assert!(validate_provider_tool_call(&call).is_err()); } diff --git a/crates/ironclaw_threads/src/tool_result_reference.rs b/crates/ironclaw_threads/src/tool_result_reference.rs index e111192223c..092c561635a 100644 --- a/crates/ironclaw_threads/src/tool_result_reference.rs +++ b/crates/ironclaw_threads/src/tool_result_reference.rs @@ -880,13 +880,20 @@ mod tests { #[test] fn provider_reference_validation_rejects_sensitive_arguments_and_text() { + // Arguments carrying a real secret-like token are rejected by the + // entropy-based leak scan, which is the canonical guard after #5001 + // dropped the crude bare-word substring markers. let mut envelope = provider_reference(); let api_key = format!("sk-proj-{}", "a".repeat(24)); envelope.arguments = serde_json::json!({"api_key": api_key}); assert!(envelope.validate().is_err()); + // Provider reasoning text flows through the same leak scan, so a leaked + // secret-like token there is rejected even though bare words like + // "stack trace" are now intentionally allowed (#5001, PinchBench bucket D). let mut envelope = provider_reference(); - envelope.response_reasoning = Some("raw provider error included a stack trace".to_string()); + envelope.response_reasoning = + Some(format!("provider error leaked sk-proj-{}", "b".repeat(24))); assert!(envelope.validate().is_err()); } diff --git a/scripts/ci/classify-test-scope.sh b/scripts/ci/classify-test-scope.sh index f58f394ebc3..91626467b58 100755 --- a/scripts/ci/classify-test-scope.sh +++ b/scripts/ci/classify-test-scope.sh @@ -33,7 +33,7 @@ is_shared_test_path() { scripts/ci/classify-test-scope.sh|scripts/ci/test-classify-test-scope.sh|scripts/ci/package-feature-flags.sh) return 0 ;; - .github/workflows/test.yml|.github/workflows/reborn-tests.yml|.github/workflows/reborn-integration.yml|.github/workflows/reborn-e2e.yml|.github/workflows/nightly-deep-ci.yml) + .github/workflows/test.yml|.github/workflows/reborn-tests.yml|.github/workflows/reborn-e2e.yml|.github/workflows/nightly-deep-ci.yml) return 0 ;; crates/ironclaw_common/*|crates/ironclaw_host_api/*|crates/ironclaw_host_runtime/*|crates/ironclaw_loop_support/*)