From adac2e0b4ef1a77a190098e95359199c598d6483 Mon Sep 17 00:00:00 2001 From: Ouroboros Date: Sun, 26 Apr 2026 17:57:30 +0300 Subject: [PATCH] =?UTF-8?q?v5.1.0:=20feat(chat+ci)=20=E2=80=94=20selective?= =?UTF-8?q?=20port=20from=20PR=20#25=20+=20chat=20bottom-fade=20layer=20fi?= =?UTF-8?q?x=20+=20retire=20ouroboros-three-layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selectively port 5 PR #25 commits into ouroboros (clipboard image paste, autocorrect-off on chat textarea, integration-test CI tier, optional macOS code signing & notarization, secrets→env fix for step-level if-conditions in GitHub Actions. Plus chat bottom gradient migration from #chat-input-area's background to a dedicated .chat-bottom-fade sibling layer (z-index 4, pointer-events:none) so the textarea no longer optically sinks into the dense end of the gradient. Plus retire ouroboros-three-layer as a dev branch — ouroboros is now the single dev branch. (1) Clipboard image paste: web/modules/chat.js registers a paste listener on #chat-input that scans e.clipboardData.items for image/*, calls getAsFile(), wraps as File(clipboard-.), and stages via the same pendingAttachment slot the paperclip uses (no inline upload — uploads when Send/Enter fires). Non-image paste falls through natively. The paperclip change handler was extracted into a shared stagePendingFile() helper so both entry points are identical. The textarea gains autocorrect=off autocapitalize=off spellcheck=false so code/identifiers/slash-commands are not silently rewritten by the browser. (2) Chat bottom-fade layer: web/style.css strips the linear-gradient background and mask-image from #chat-input-area (which keeps z-index 5), and adds a new dedicated sibling .chat-bottom-fade (position:absolute; bottom:0; pointer-events:none; z-index:4; height:200px) below the input dock. Mobile @media (max-width: 640px) uses calc(200px + env(safe-area-inset-bottom, 0px)) so the fade fully covers the iOS-home-indicator safe area at the worst-case input-area state (attachment + fully-expanded textarea). (3) Integration tier in CI (Tier 2.5): new integration-test job runs pytest tests/test_provider_integration.py -m integration on ubuntu-latest with OPENROUTER_API_KEY/OPENAI_API_KEY/ANTHROPIC_API_KEY in repo secrets. Triggered on push to main / ouroboros / ouroboros-stable, on workflow_dispatch, and on tag v*. Locally the pytest marker plus addopts -m 'not integration' in pyproject.toml exclude the tests from default runs. (4) Optional macOS code signing & notarization (Build tier): when BUILD_CERTIFICATE_BASE64 / P12_PASSWORD / KEYCHAIN_PASSWORD / APPLE_TEAM_ID are configured as repo secrets, the build job creates a temporary keychain, imports the Developer ID certificate, and runs bash build.sh (which signs .app and .dmg via env-overridable SIGN_IDENTITY). With APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD also present, build.sh runs xcrun notarytool submit --wait followed by xcrun stapler staple. Stapler/notarytool failures are wrapped in if/else (set -e exempt) so transient Apple-CDN flakes become warnings instead of dropping the macOS DMG from the release. A NOTARIZE_OUTCOME enum drives a 4-case summary cascade (success / staple_failed / submit_failed / unconfigured) plus a defensive *) arm. With no Apple secrets the build falls back to OUROBOROS_SIGN=0 bash build.sh (identical to v5.0.0). Cleanup keychain step runs with if: always() && matrix.os == 'macos-latest' && env.BUILD_CERTIFICATE_BASE64 != '' so signing material never persists across runs and the bash-only security delete-keychain invocation never fires on Linux/Windows shards. The Import step sets trap 'rm -f "$CERTIFICATE_PATH"' EXIT so the temporary .p12 is removed on every exit, including a set -e abort mid-import. (5) secrets→env fix for step-level if (v4.47.1 lesson): GitHub Actions rejects secrets.* references inside step-level if expressions (Unrecognized named-value: secrets). All Apple signing secrets are mapped at the build job's env: block with a ${{ matrix.os == 'macos-latest' && secrets.X || '' }} guard so non-macOS shards receive empty strings — Linux/Windows never see the signing material. Step-level if reads env.* instead. New docs/DEVELOPMENT.md section GitHub Actions: secrets in step-level if conditions formalizes the rule with worked examples. (6) Tests: tests/test_chat_logs_ui.py gains 3 new tests (test_chat_input_disables_autocorrect, test_clipboard_paste_handler_exists, test_chat_bottom_fade_is_separate_layer); tests/test_build_scripts.py gains a new TestMacOSSigning class with 7 contract tests (job-level secrets env mapping with matrix.os guard, no secrets.* in any if-block, Import step gates on full secret set, Cleanup keychain always() + matrix.os + env guard, build.sh SIGN_IDENTITY env override, notarytool + stapler optional gate, stapler-failure-as-soft-warning regression); tests/test_provider_integration.py is added new with 6 tuple-aware tests (OpenRouter / OpenAI / Anthropic × {basic, isolation}) handling the post-v4.44.0 LLMClient.chat() (msg, usage) tuple plus Anthropic's list-of-blocks content. The existing test_chat_floating_overlays_have_readable_glass_backing was updated for the migrated bottom-fade contract (asserts no backdrop-filter on .chat-bottom-fade across base + mobile @media rules). (7) Branch consolidation: ouroboros-three-layer is retired as a dev branch. ouroboros is now the single dev branch. .github/workflows/ci.yml (Tier 1 quick-test trigger + path-filter branches list + build job's OUROBOROS_MANAGED_SOURCE_BRANCH default), build.sh / build_linux.sh / build_windows.ps1 (each script's ${OUROBOROS_MANAGED_SOURCE_BRANCH:-...} default), and four test files (test_release_workflow.py, test_launcher_sync.py, test_git_ops_recovery.py, test_build_repo_bundle.py — 21 occurrences total) all switch from ouroboros-three-layer to ouroboros. Historical references in older changelog rows (v4.50.0-rc.7) and in ouroboros/* module comments about the Phase 2/3 three-layer architecture refactor are intentionally preserved — those describe the architectural refactor, not the dev branch name. The remote managed/ouroboros-three-layer branch is deleted in the same release. Adversarial multimodel review (gemini-2.5/gpt-5.5/claude-opus-4.7 critics in parallel, full-context, 4 rounds): 32 findings total → 18 fixed, 14 rejected/deferred with explicit per-finding reasoning. All three critics independently reach SAFE TO COMMIT after round 4. Ouroboros triad+scope review (production code path parallel_review.run_parallel_review with full-repo pack, 2 rounds): 4 findings (2 scope-critical + 1 scope-advisory + 1 triad-advisory) → all 4 fixed. Round 1 caught matrix-shard secret leak; round 2 caught documentation/runtime command mismatch + cert-file cleanup gap on set -e failure. VERSION 5.0.0 → 5.1.0 (MINOR: additive features + UX/CI polish, no breaking change). Release invariant synchronised: VERSION, pyproject.toml [project].version, README badge, docs/ARCHITECTURE.md header — all 5.1.0. Note on changelog rolloff: the v4.50.0-rc.2 minor entry is rolled off proactively to keep one slot below the P7 5-minor-row cap. Its full body remains at git tag v4.50.0-rc.2. EOF ) --- .github/workflows/ci.yml | 132 ++++++++++++-- README.md | 35 +++- VERSION | 2 +- build.sh | 83 ++++++++- build_linux.sh | 2 +- build_windows.ps1 | 2 +- docs/ARCHITECTURE.md | 64 ++++++- docs/DEVELOPMENT.md | 83 +++++++++ pyproject.toml | 10 +- tests/test_build_repo_bundle.py | 24 +-- tests/test_build_scripts.py | 267 +++++++++++++++++++++++++++++ tests/test_chat_logs_ui.py | 154 ++++++++++++++++- tests/test_git_ops_recovery.py | 12 +- tests/test_launcher_sync.py | 4 +- tests/test_provider_integration.py | 175 +++++++++++++++++++ tests/test_release_workflow.py | 2 +- web/modules/chat.js | 50 +++++- web/style.css | 51 ++++-- 18 files changed, 1072 insertions(+), 80 deletions(-) create mode 100644 tests/test_provider_integration.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03c5d5ea1..ecb768a81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,13 @@ -# Ouroboros CI — Three-tier cross-platform testing and release pipeline +# Ouroboros CI — Four-tier cross-platform testing and release pipeline # -# Tier 1: Every push to ouroboros / ouroboros-three-layer (code paths) → Ubuntu-only tests (~1 min) -# Tier 2: Push to ouroboros-stable / manual / tag → Full 3-OS matrix (~5 min) -# Tier 3: Tag v* → Full matrix + build artifacts + GitHub Release (~15 min) +# Tier 1 (Quick): Push to ouroboros (code paths) → Ubuntu-only tests (~1 min) +# Tier 2 (Full): Push to ouroboros-stable / manual / tag → Full 3-OS matrix (~5 min) +# Tier 2.5 (Integration): Push to main / ouroboros / ouroboros-stable / manual / tag → Real-provider tests (~2 min) +# Tier 3 (Build+Release): Tag v* → PyInstaller + GitHub Release (~15 min) +# +# Tier 2.5 requires OPENROUTER_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY in +# repository secrets and runs the `integration` pytest marker; locally these +# tests are excluded by `addopts = -m 'not integration'` in pyproject.toml. name: CI @@ -10,7 +15,7 @@ name: CI # This ensures tag pushes always fire (even if only VERSION/README changed). on: push: - branches: [ouroboros, ouroboros-three-layer, ouroboros-stable] + branches: [main, ouroboros, ouroboros-stable] paths: - 'ouroboros/**' - 'supervisor/**' @@ -37,15 +42,12 @@ on: jobs: # ────────────────────────────────────────────────────────────────── - # Tier 1: Quick tests on Ubuntu (every push to ouroboros / ouroboros-three-layer) + # Tier 1: Quick tests on Ubuntu (every push to ouroboros) # ────────────────────────────────────────────────────────────────── quick-test: if: | github.event_name == 'push' - && ( - github.ref == 'refs/heads/ouroboros' - || github.ref == 'refs/heads/ouroboros-three-layer' - ) + && github.ref == 'refs/heads/ouroboros' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -86,6 +88,42 @@ jobs: - name: Run tests run: python -m pytest tests/ -q --tb=short + # ────────────────────────────────────────────────────────────────── + # Tier 2.5: Integration tests against real provider APIs + # Triggered on push to main / ouroboros / ouroboros-stable, manual, + # or tag v*. Requires OPENROUTER_API_KEY / OPENAI_API_KEY / + # ANTHROPIC_API_KEY in repository secrets. The `integration` pytest + # marker (in pyproject.toml) controls inclusion via `-m integration`; + # within an included test file, missing-key skipping is done by per- + # test `@pytest.mark.skipif(not os.environ.get(KEY))` decorators (see + # tests/test_provider_integration.py). NOT a `needs:` of build/ + # release: a provider outage must not block a tagged release. + # ────────────────────────────────────────────────────────────────── + integration-test: + if: | + github.event_name == 'workflow_dispatch' + || github.ref == 'refs/heads/main' + || github.ref == 'refs/heads/ouroboros' + || github.ref == 'refs/heads/ouroboros-stable' + || startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install pytest + - name: Run integration tests + env: + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: python -m pytest tests/test_provider_integration.py -m integration -q --tb=short + # ────────────────────────────────────────────────────────────────── # Tier 3: Build & Release (tag push only) # ────────────────────────────────────────────────────────────────── @@ -138,8 +176,37 @@ jobs: artifact: zip runs-on: ${{ matrix.os }} env: - OUROBOROS_MANAGED_SOURCE_BRANCH: ouroboros-three-layer + OUROBOROS_MANAGED_SOURCE_BRANCH: ouroboros OUROBOROS_RELEASE_TAG: ${{ github.ref_name }} + # Apple signing secrets at JOB LEVEL with a per-matrix-shard guard. + # + # Step-level `if:` conditions can only read `env.*`, never `secrets.*` + # directly (GitHub Actions rejects the workflow with "Unrecognized + # named-value: 'secrets'"). See docs/DEVELOPMENT.md::"GitHub Actions: + # secrets in step-level if conditions". + # + # The `matrix.os == 'macos-latest' && ... || ''` GHA expression keeps + # the Apple signing/notarization values **scoped to the macOS shard + # only** — Linux and Windows shards (which run `build_linux.sh` and + # `build_windows.ps1` respectively, neither of which needs Apple + # creds) receive empty strings. This avoids exposing the signing + # material to non-macOS build subprocesses where it has no business + # being. When a secret is not configured even on macOS, the value + # is also empty string (not unset), and the gate `env.X != ''` + # evaluates false — the signing/notarization steps skip cleanly. + BUILD_CERTIFICATE_BASE64: ${{ matrix.os == 'macos-latest' && secrets.BUILD_CERTIFICATE_BASE64 || '' }} + P12_PASSWORD: ${{ matrix.os == 'macos-latest' && secrets.P12_PASSWORD || '' }} + KEYCHAIN_PASSWORD: ${{ matrix.os == 'macos-latest' && secrets.KEYCHAIN_PASSWORD || '' }} + APPLE_TEAM_ID: ${{ matrix.os == 'macos-latest' && secrets.APPLE_TEAM_ID || '' }} + APPLE_ID: ${{ matrix.os == 'macos-latest' && secrets.APPLE_ID || '' }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ matrix.os == 'macos-latest' && secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }} + # SIGN_IDENTITY is a forks-friendly override: when a fork configures + # a Developer ID secret whose CN differs from the upstream default + # (e.g. "Developer ID Application: ()"), + # they set `SIGN_IDENTITY` as a repository secret and codesign in + # build.sh picks it up via `${SIGN_IDENTITY:-...}`. Same matrix.os + # guard so Linux/Windows shards never see it. + SIGN_IDENTITY: ${{ matrix.os == 'macos-latest' && secrets.SIGN_IDENTITY || '' }} steps: - uses: actions/checkout@v4 with: @@ -184,10 +251,49 @@ jobs: shell: pwsh run: .\scripts\download_python_standalone.ps1 - # —— macOS build —— + # —— macOS: import signing certificate (only when ALL four signing + # secrets are present at job level — see env: block above) + - name: Import Apple signing certificate + if: matrix.os == 'macos-latest' && env.BUILD_CERTIFICATE_BASE64 != '' && env.P12_PASSWORD != '' && env.KEYCHAIN_PASSWORD != '' && env.APPLE_TEAM_ID != '' + run: | + set -euo pipefail + CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12" + KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" + # Always remove the .p12 on EXIT, including failure mid-import: + # `set -e` would otherwise abort before the trailing `rm -f` and + # leave the certificate blob on the runner until cleanup. The + # later `Cleanup keychain` step only handles the keychain itself. + trap 'rm -f "$CERTIFICATE_PATH"' EXIT + echo "${BUILD_CERTIFICATE_BASE64}" | base64 --decode > "$CERTIFICATE_PATH" + security create-keychain -p "${KEYCHAIN_PASSWORD}" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "$KEYCHAIN_PATH" + security import "$CERTIFICATE_PATH" -P "${P12_PASSWORD}" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security list-keychain -d user -s "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: -k "${KEYCHAIN_PASSWORD}" "$KEYCHAIN_PATH" >/dev/null + security find-identity -v -p codesigning "$KEYCHAIN_PATH" + + # —— macOS build (signed + optionally notarized when secrets are + # present, otherwise unsigned). build.sh reads the same + # env vars from the job-level env block above. - name: Build macOS app if: matrix.os == 'macos-latest' - run: OUROBOROS_SIGN=0 bash build.sh + run: | + if [ -n "${BUILD_CERTIFICATE_BASE64:-}" ] && [ -n "${P12_PASSWORD:-}" ] && [ -n "${KEYCHAIN_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then + echo "Signing certificate detected — building with codesign + (optional) notarization" + bash build.sh + else + echo "No signing secrets — building unsigned (OUROBOROS_SIGN=0)" + OUROBOROS_SIGN=0 bash build.sh + fi + + # —— macOS: cleanup keychain (always, even on build failure) so the + # temporary signing material never persists across runs. + - name: Cleanup keychain + if: always() && matrix.os == 'macos-latest' && env.BUILD_CERTIFICATE_BASE64 != '' + run: | + KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" + security delete-keychain "$KEYCHAIN_PATH" || true # —— Linux build —— - name: Build Linux binary diff --git a/README.md b/README.md index 49b3d57ab..3ba5ffb74 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![macOS 12+](https://img.shields.io/badge/macOS-12%2B-black.svg)](https://github.com/joi-lab/ouroboros-desktop/releases) [![Linux](https://img.shields.io/badge/Linux-x86__64-orange.svg)](https://github.com/joi-lab/ouroboros-desktop/releases) [![Windows](https://img.shields.io/badge/Windows-x64-blue.svg)](https://github.com/joi-lab/ouroboros-desktop/releases) -[![Version 5.0.0](https://img.shields.io/badge/version-5.0.0-green.svg)](VERSION) +[![Version 5.1.0](https://img.shields.io/badge/version-5.1.0-green.svg)](VERSION) A self-modifying AI agent that writes its own code, rewrites its own mind, and evolves autonomously. Born February 16, 2026. @@ -231,6 +231,37 @@ Output: `dist/Ouroboros-.dmg` configured local Developer ID identity; set `OUROBOROS_SIGN=0` for an unsigned local release. Unsigned builds require right-click → **Open** on first launch. +#### Optional signing & notarization (env vars) + +`build.sh` honours these env overrides so the same script ships local, +shared-machine, and CI builds without forking the script: + +| Env var | Effect | +|---------|--------| +| `OUROBOROS_SIGN=0` | Skip codesigning entirely (unsigned `.app` + `.dmg`). | +| `SIGN_IDENTITY="Developer ID Application: ()"` | Override the codesign identity. Useful for forks whose Developer ID is not the upstream default. | +| `APPLE_ID`, `APPLE_TEAM_ID`, `APPLE_APP_SPECIFIC_PASSWORD` | When all three are set, after codesign the DMG is submitted to Apple via `xcrun notarytool submit ... --wait` and stapled with `xcrun stapler staple` so receivers do not need right-click → **Open**. Missing any one falls back to "signed but not notarized" (no Apple-side ticket exists). | + +**Forks: enabling signed CI builds.** The CI release flow +(`.github/workflows/ci.yml::build`) wires the build-script env vars above +from GitHub repository secrets, plus a small set of CI-only secrets that +import the Developer ID certificate into a temporary keychain on the +macOS runner. To exercise the signed-build path in a fork, configure +**all four** of the following as repository secrets (Settings → Secrets +and variables → Actions): `BUILD_CERTIFICATE_BASE64` (base64-encoded +`.p12`), `P12_PASSWORD`, `KEYCHAIN_PASSWORD` (an arbitrary passphrase +the workflow uses for its temporary keychain), and `APPLE_TEAM_ID`. Add +`APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` to additionally enable +notarization. If your Developer ID identity differs from the upstream +default, also set `SIGN_IDENTITY` (e.g. +`Developer ID Application: ()`). With no +Apple secrets configured the build job falls through to +`OUROBOROS_SIGN=0 bash build.sh` and ships an unsigned DMG identical to +v5.0.0 behaviour. See `docs/ARCHITECTURE.md` §8.1 and +`docs/DEVELOPMENT.md::"GitHub Actions: secrets in step-level if conditions"` +for the rationale (job-level `env:` mapping so step-level `if:` can read +`env.*`; GHA rejects `secrets.*` in step `if:`). + ### Linux (.tar.gz) ```bash @@ -413,11 +444,11 @@ Full text: [BIBLE.md](BIBLE.md) | Version | Date | Description | |---------|------|-------------| +| 5.1.0 | 2026-04-26 | **feat(chat+ci): selective port from PR #25 + chat bottom-fade layer fix.** (1) **Clipboard image paste** — `web/modules/chat.js` registers a `paste` listener on `#chat-input` that scans `e.clipboardData.items` for `image/*`, calls `getAsFile()`, wraps the blob as `File("clipboard-.")`, and stages it through the same `pendingAttachment` slot the paperclip button uses (no inline upload — the file uploads when Send/Enter fires, with the same offline-WS guard). `e.preventDefault()` runs only when an image item is matched, so non-image clipboard payloads still paste natively. (2) **Browser-level mangling disabled on the chat textarea** — `#chat-input` gains `autocorrect="off" autocapitalize="off" spellcheck="false"` so code, identifiers, and slash-commands are not silently rewritten. (3) **Chat bottom gradient moved to its own layer** — previously `#chat-input-area`'s `background: linear-gradient(...)` painted directly behind the textarea, which made the lower edge of the input visually dissolve into the dense end of the gradient. `web/style.css` now strips the gradient/mask from `#chat-input-area` (which keeps `z-index: 5`), and a dedicated sibling element `