diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 0ca871bd..26731813 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,25 +2,29 @@ name: Android APK on: workflow_dispatch: + inputs: + version: + description: Optional package version override (without a leading v) + required: false permissions: - contents: write + contents: read jobs: build-android: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 cache: pnpm - - uses: actions/setup-java@v4 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: temurin java-version: 21 @@ -36,24 +40,26 @@ jobs: - name: Sync Capacitor Android run: pnpm exec cap sync android + env: + OPENGUI_VERSION: ${{ github.event.inputs.version || '' }} - name: Build release APK run: cd android && ./gradlew assembleRelease + env: + OPENGUI_VERSION: ${{ github.event.inputs.version || '' }} - name: Rename release APK + env: + OPENGUI_VERSION: ${{ github.event.inputs.version || '' }} run: | + version=${OPENGUI_VERSION:-$(node -p "require('./package.json').version")} mkdir -p release-android - cp android/app/build/outputs/apk/release/*.apk "release-android/OpenGUI-${GITHUB_REF_NAME}-android-release.apk" + cp android/app/build/outputs/apk/release/*.apk "release-android/OpenGUI-${version#v}-android-release.apk" - name: Upload release APK artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: opengui-android-release-apk path: release-android/*.apk retention-days: 30 - - - name: Attach APK to GitHub Release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v2 - with: - files: release-android/*.apk + if-no-files-found: error diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c064ba89..9f4a5747 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,416 +1,297 @@ -name: Build +name: Release candidate on: + workflow_dispatch: + inputs: + version: + description: Package version to validate and build (without a leading v) + required: true + default: 0.6.0-rc.1 push: tags: ["v*"] +permissions: + contents: read + +concurrency: + group: release-candidate-${{ github.ref }} + cancel-in-progress: false + jobs: preflight: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Lint - run: pnpm exec vp lint - - - name: Check - run: pnpm exec vp check - - - name: Build frontend - run: pnpm exec vp build - - - name: Run unit tests - run: pnpm exec vp test - - - name: Upload frontend build - uses: actions/upload-artifact@v4 + - run: pnpm install --frozen-lockfile + - name: Verify requested version + shell: bash + env: + REQUESTED_VERSION: ${{ github.event_name == 'push' && github.ref_name || inputs.version }} + run: | + package_version=$(node -p "require('./package.json').version") + requested_version=${REQUESTED_VERSION#v} + if ! [[ "$requested_version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + echo "Version is not strict SemVer: $requested_version" >&2 + exit 1 + fi + test "$package_version" = "$requested_version" || { + echo "Requested version $requested_version does not match package.json $package_version" >&2 + exit 1 + } + - run: pnpm run check + - run: pnpm run test + - run: pnpm run slop-check + - run: pnpm run build + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: frontend-build path: | dist dist-electron retention-days: 1 + if-no-files-found: error - create-release: - if: startsWith(github.ref, 'refs/tags/v') - needs: preflight - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Create GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || \ - gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --generate-notes - - build-linux: - needs: [preflight, create-release] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Cache node_modules - id: cache-nm - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-nm-${{ hashFiles('pnpm-lock.yaml') }} - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Cache Electron binaries - uses: actions/cache@v4 - with: - path: | - ~/.cache/electron - ~/.cache/electron-builder - key: electron-cache-${{ runner.os }}-x64-${{ hashFiles('package.json') }} - - - name: Restore frontend build - uses: actions/download-artifact@v4 - with: - name: frontend-build - - - name: Package Linux artifacts - run: pnpm exec electron-builder --linux deb AppImage --publish never - - - name: Upload Linux artifacts - uses: actions/upload-artifact@v4 - with: - name: linux-release - path: | - release/*.deb - release/*.AppImage - retention-days: 30 - - - name: Attach Linux artifacts to GitHub Release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v2 - with: - files: | - release/*.deb - release/*.AppImage - - build-mac: + build-desktop: needs: preflight strategy: - matrix: - arch: [x64, arm64] fail-fast: false - runs-on: macos-latest + matrix: + include: + - runner: ubuntu-latest + name: linux + command: pnpm exec electron-builder --linux deb AppImage --publish never + artifacts: | + release/*.deb + release/*.AppImage + release/latest-linux.yml + - runner: windows-latest + name: windows + command: pnpm exec electron-builder --win nsis --publish never + artifacts: | + release/*.exe + release/*.blockmap + release/latest.yml + - runner: macos-latest + name: mac-x64 + command: pnpm exec electron-builder --mac dmg zip --x64 --publish never + artifacts: | + release/*.dmg + release/*.zip + release/*.blockmap + release/latest-mac*.yml + - runner: macos-latest + name: mac-arm64 + command: pnpm exec electron-builder --mac dmg zip --arm64 --publish never + artifacts: | + release/*.dmg + release/*.zip + release/*.blockmap + release/latest-mac*.yml + runs-on: ${{ matrix.runner }} steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 cache: pnpm - - - name: Cache node_modules - id: cache-nm - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-${{ matrix.arch }}-nm-${{ hashFiles('pnpm-lock.yaml') }} - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Cache Electron binaries - uses: actions/cache@v4 - with: - path: | - ~/.cache/electron - ~/.cache/electron-builder - key: electron-cache-${{ runner.os }}-${{ matrix.arch }}-${{ hashFiles('package.json') }} - - - name: Restore frontend build - uses: actions/download-artifact@v4 + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: frontend-build - - - name: Package mac ${{ matrix.arch }} - run: pnpm exec electron-builder --mac dmg zip --${{ matrix.arch }} --publish never + - name: Package ${{ matrix.name }} without publishing + run: ${{ matrix.command }} env: CSC_IDENTITY_AUTO_DISCOVERY: "false" - - - name: Normalize update manifest filename - run: | - if ls release/latest-mac-*.yml >/dev/null 2>&1; then - cp release/latest-mac-*.yml release/latest-mac.yml - fi - ls -la release/latest-mac*.yml - - - name: Upload mac ${{ matrix.arch }} artifacts - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: mac-release-${{ matrix.arch }} - path: release/* + name: desktop-${{ matrix.name }} + path: ${{ matrix.artifacts }} retention-days: 30 + if-no-files-found: error - build-windows: - needs: [preflight, create-release] - runs-on: windows-latest - permissions: - contents: write + build-android: + needs: preflight + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Cache Electron binaries - uses: actions/cache@v4 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: - path: | - ~/.cache/electron - ~/.cache/electron-builder - key: electron-cache-${{ runner.os }}-x64-${{ hashFiles('package.json') }} - - - name: Restore frontend build - uses: actions/download-artifact@v4 + distribution: temurin + java-version: 21 + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: frontend-build - - - name: Package .exe (NSIS) - run: pnpm exec electron-builder --win nsis --publish never - - - name: Verify Windows updater metadata - shell: pwsh + - run: pnpm exec cap sync android + - run: cd android && ./gradlew assembleRelease + env: + OPENGUI_VERSION: ${{ github.event_name == 'push' && github.ref_name || inputs.version }} + - name: Collect APK + shell: bash run: | - $latest = "release/latest.yml" - if (!(Test-Path $latest)) { - throw "Missing $latest" - } - - $content = Get-Content $latest -Raw - $paths = @() - foreach ($line in ($content -split "`n")) { - if ($line -match '^\s*-?\s*url:\s*(.+)$') { - $paths += $Matches[1].Trim().Trim('"').Trim("'") - } - if ($line -match '^path:\s*(.+)$') { - $paths += $Matches[1].Trim().Trim('"').Trim("'") - } - } - - if ($paths.Count -eq 0) { - throw "No updater asset paths found in $latest" - } - - foreach ($asset in ($paths | Select-Object -Unique)) { - $assetPath = Join-Path "release" $asset - if (!(Test-Path $assetPath)) { - throw "Windows updater metadata references missing asset: $asset" - } - } - - - name: Upload Windows updater artifacts - uses: actions/upload-artifact@v4 + version='${{ github.event_name == 'push' && github.ref_name || inputs.version }}' + mkdir -p release-android + cp android/app/build/outputs/apk/release/*.apk "release-android/OpenGUI-${version#v}-android-release.apk" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: windows-nsis - path: | - release/*.exe - release/*.blockmap - release/latest.yml + name: android + path: release-android/*.apk retention-days: 30 + if-no-files-found: error - - name: Attach Windows artifacts to GitHub Release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v2 + build-docker: + needs: preflight + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + suffix: amd64 + - platform: linux/arm64 + suffix: arm64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: - files: | - release/*.exe - release/*.blockmap - release/latest.yml - - build-android: - needs: [preflight, create-release] + context: . + platforms: ${{ matrix.platform }} + load: true + push: false + tags: opengui-candidate:${{ matrix.suffix }} + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Smoke image health + shell: bash + run: | + cid=$(docker run -d --platform '${{ matrix.platform }}' -p 127.0.0.1::3000 \ + -e OPENGUI_ALLOWED_ROOTS=/workspace "opengui-candidate:${{ matrix.suffix }}") + trap 'docker logs "$cid"; docker rm -f "$cid"' EXIT + port=$(docker port "$cid" 3000/tcp | awk -F: '{print $NF}') + for attempt in {1..60}; do + if curl --fail --silent "http://127.0.0.1:$port/api/health" >/dev/null; then exit 0; fi + sleep 2 + done + echo "Container did not become healthy" >&2 + exit 1 + + prepare-candidate: + needs: [build-desktop, build-android] runs-on: ubuntu-latest - permissions: - contents: write steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 cache: pnpm - - - uses: actions/setup-java@v4 + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - distribution: temurin - java-version: 21 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Restore frontend build - uses: actions/download-artifact@v4 + pattern: desktop-* + path: artifacts + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: frontend-build - - - name: Sync Capacitor Android - run: pnpm exec cap sync android - - - name: Build release APK - run: cd android && ./gradlew assembleRelease - - - name: Rename release APK + name: android + path: artifacts/android + - name: Merge macOS updater metadata run: | - mkdir -p release-android - cp android/app/build/outputs/apk/release/*.apk "release-android/OpenGUI-${GITHUB_REF_NAME}-android-release.apk" - - - name: Upload Android APK artifact - uses: actions/upload-artifact@v4 + node scripts/merge-latest-mac.mjs \ + --x64-dir artifacts/desktop-mac-x64 \ + --arm64-dir artifacts/desktop-mac-arm64 \ + --out-dir artifacts/desktop-mac-x64 + - name: Verify updater metadata and generate checksums + shell: bash + run: | + test -f artifacts/desktop-linux/latest-linux.yml + test -f artifacts/desktop-windows/latest.yml + test -f artifacts/desktop-mac-x64/latest-mac.yml + find artifacts -type f ! -name 'latest-mac-*.yml' -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS.txt + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: opengui-android-release-apk - path: release-android/*.apk + name: release-candidate + path: | + artifacts + SHA256SUMS.txt retention-days: 30 - - - name: Attach Android APK to GitHub Release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v2 - with: - files: release-android/*.apk + if-no-files-found: error publish-docker: if: startsWith(github.ref, 'refs/tags/v') - needs: preflight + needs: [build-desktop, build-android, build-docker, prepare-candidate] runs-on: ubuntu-latest + environment: release permissions: contents: read packages: write steps: - - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GHCR - uses: docker/login-action@v3 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@v5 + - id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 with: images: ghcr.io/akemmanuel/opengui tags: | type=ref,event=tag type=semver,pattern={{version}} - type=raw,value=latest + type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }} labels: | org.opencontainers.image.title=OpenGUI - org.opencontainers.image.description=OpenGUI web command center for coding agents + org.opencontainers.image.description=OpenGUI first-party agent Host and web interface org.opencontainers.image.source=https://github.com/akemmanuel/OpenGUI - org.opencontainers.image.url=https://github.com/akemmanuel/OpenGUI org.opencontainers.image.licenses=MIT - - - name: Build and push Docker image - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . platforms: linux/amd64,linux/arm64 push: true + provenance: mode=max + sbom: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha - cache-to: type=gha,mode=max - publish-mac: + create-release: if: startsWith(github.ref, 'refs/tags/v') - needs: [build-mac, create-release] + needs: [publish-docker, prepare-candidate] runs-on: ubuntu-latest + environment: release permissions: contents: write steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Install script dependencies - run: pnpm install --frozen-lockfile - - - name: Download mac x64 artifacts - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: mac-release-x64 - path: artifacts/mac-release - - - name: Download mac arm64 artifacts - uses: actions/download-artifact@v4 - with: - name: mac-release-arm64 - path: artifacts/mac-release-arm64 - - - name: List downloaded artifacts - run: | - echo "=== mac-release ===" - ls -la artifacts/mac-release/ 2>/dev/null || echo "(empty)" - echo "=== mac-release-arm64 ===" - ls -la artifacts/mac-release-arm64/ 2>/dev/null || echo "(empty)" - - - name: Merge mac update manifests + name: release-candidate + path: candidate + - name: Create prerelease and upload validated artifacts + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + shell: bash run: | - node scripts/merge-latest-mac.mjs \ - --x64-dir artifacts/mac-release \ - --arm64-dir artifacts/mac-release-arm64 \ - --out-dir artifacts/mac-release - - - name: Attach mac artifacts to GitHub Release - uses: softprops/action-gh-release@v2 - with: - files: | - artifacts/mac-release/*.dmg - artifacts/mac-release/*.zip - artifacts/mac-release/*.blockmap - artifacts/mac-release-arm64/*.dmg - artifacts/mac-release-arm64/*.zip - artifacts/mac-release-arm64/*.blockmap - artifacts/mac-release/latest-mac.yml + if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + echo "Release $GITHUB_REF_NAME already exists; refusing to mutate ambiguous state" >&2 + exit 1 + fi + args=("$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --generate-notes) + [[ "$GITHUB_REF_NAME" == *-* ]] && args+=(--prerelease) + gh release create "${args[@]}" candidate/SHA256SUMS.txt $(find candidate/artifacts -type f ! -name 'latest-mac-*.yml' -print) diff --git a/.github/workflows/ultracode-quality.yml b/.github/workflows/ultracode-quality.yml index 03a5ed69..a20b78bc 100644 --- a/.github/workflows/ultracode-quality.yml +++ b/.github/workflows/ultracode-quality.yml @@ -3,20 +3,9 @@ name: Ultracode quality gate on: workflow_dispatch: pull_request: - paths: - - "main/**" - - "main.ts" - - "preload.ts" - - "settings-store.ts" - - "src/**" - - "server/**" - - "packages/**" - - "scripts/**" - - "package.json" - - "pnpm-lock.yaml" - - "vite.config.ts" - - "vite.electron.config.ts" - - ".github/workflows/ultracode-quality.yml" + +permissions: + contents: read concurrency: group: ultracode-quality-${{ github.workflow }}-${{ github.ref }} @@ -29,11 +18,11 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 cache: pnpm @@ -41,5 +30,29 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Run Ultracode quality workflow - run: pnpm run ultracode:quality + - name: Check formatting, lint, and types + run: pnpm run check + + - name: Run tests + run: pnpm run test + + - name: Run architecture guards + run: pnpm run slop-check + + - name: Build production bundles + run: pnpm run build + + windows-native: + name: Windows shell and process tests + runs-on: windows-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Exercise native shell resolution and process-tree cancellation + run: pnpm vp test scripts/process-tree.test.ts main/desktop-shell.test.ts packages/harness/src/tools/shell-resolution.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..1445a173 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +## 0.6.0-rc.1 — release candidate + +This candidate replaces the external coding-agent integration architecture with the first-party +OpenGUI Host and Harness. It is a minor release because it adds substantial product capabilities +and changes Session persistence; it is not yet the stable 0.6.0 release. + +### Highlights + +- First-party Harness with durable Host-owned Sessions, automatic context compaction, Agent + Skills, image input/tool support, and the built-in `read`, `write`, `edit`, and `shell` tools. +- Multi-user Remote Hosts with Accounts, invites, roles, explicit path/model/Session grants, + Host API keys, and private Sessions by default. +- Model backends and member-facing model offerings with Host/Team/User ownership planes. +- Host-owned MCP stdio and Streamable HTTP connections, bounded discovery, connection health, + last-known-good catalogs, and recoverable tool failures. +- Refined sidebar Project/Session hierarchy and transcript, model, provider, and settings work. + +### Breaking migration note + +Sessions created by the old external-Harness architecture are **not imported** into the new +first-party Harness database. OpenGUI does not delete or modify the external Harness source files, +but those Sessions do not appear in the new sidebar. Treat the old application data as a separate +backup if it must be retained. There is no supported automatic importer in this candidate. + +The exported MCP broker contract is also new for 0.6: consumers must call `refresh(scope)` before +expecting `catalog(scope)` to contain tools, and catalog diagnostics are scoped to the actor and +Session. This is an intentional prerelease API boundary rather than a 0.5-compatible adapter. + +Remote Host operators should back up the Host data directory before upgrading and test the +candidate against a copy. Desktop, Web/Docker, and Android packaging still require platform smoke +testing; macOS artifacts are unsigned unless release signing is configured, and the Android +candidate currently uses the repository's temporary debug signing configuration. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64c1b3c4..fa97317b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,8 @@ ## Setup -Install Node.js 22.5 or newer and pnpm 11.8.0, then run: +The application supports Node.js 22.19 or newer. Contributors and release gates use Node.js 24 +because the complete QA toolchain requires it. Install Node.js 24 and pnpm 11.8.0, then run: ```bash pnpm install @@ -18,13 +19,15 @@ OpenGUI has one product path: 1. The preserved React frontend calls the OpenGUI Host. 2. The Host owns projects, model connections, Sessions, and transport. 3. The first-party Harness owns execution and the append-oriented SQLite Session log. -4. Model turns can invoke only `read`, `write`, `edit`, and `shell`. +4. Model turns can invoke the four native tools (`read`, `write`, `edit`, and `shell`) plus + explicitly configured, Host-authorized MCP tools through the narrow seam in ADR 0015. Do not add external coding-agent SDKs, CLI adapters, compatibility facades, alternate Session -identity schemes, Git/worktree orchestration, MCP, or plugin runtimes. +identity schemes, Git/worktree orchestration, frontend-owned MCP runtimes, or plugin runtimes. Read [`CONTEXT.md`](CONTEXT.md), [`docs/architecture.md`](docs/architecture.md), and [`docs/adr/0010-first-party-opengui-harness.md`](docs/adr/0010-first-party-opengui-harness.md) +and [`docs/adr/0015-host-owned-mcp-tool-connections.md`](docs/adr/0015-host-owned-mcp-tool-connections.md) before changing product boundaries. ## Quality diff --git a/Dockerfile b/Dockerfile index 21850cdd..5c98d7e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,21 +16,17 @@ RUN apt-get update \ WORKDIR /app -ENV BUN_INSTALL=/bun -ENV PATH=/app/node_modules/.bin:$BUN_INSTALL/bin:$PATH - -RUN npm install -g bun@1.3.13 - -COPY package.json pnpm-workspace.yaml ./ -COPY scripts/ensure-electron.mjs ./scripts/ensure-electron.mjs -RUN bun install +ENV PATH=/app/node_modules/.bin:$PATH COPY . . -RUN vp build +RUN corepack enable \ + && corepack prepare pnpm@11.8.0 --activate \ + && pnpm install --frozen-lockfile \ + && pnpm vp build COPY docker/host-exec /usr/local/bin/opengui-host-exec COPY docker/entrypoint.sh /usr/local/bin/opengui-entrypoint RUN chmod +x /usr/local/bin/opengui-host-exec /usr/local/bin/opengui-entrypoint \ - && mkdir -p /usr/local/host-bin \ + && mkdir -p /usr/local/host-bin /workspace \ && for cmd in node npm pnpm python python3 bash sh rg fd make gcc g++; do ln -sf /usr/local/bin/opengui-host-exec /usr/local/host-bin/$cmd; done ENV HOST=0.0.0.0 diff --git a/README.md b/README.md index 83daf848..fd69a20f 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,14 @@ The Harness exposes four workspace tools: `read`, `write`, `edit`, and `shell`. ## Requirements -- Node.js 22.5 or newer +- Node.js 22.19 or newer (release CI uses Node.js 24) - pnpm 11.8.0 - An OpenAI-compatible model endpoint and API key +See [`docs/model-compatibility.md`](docs/model-compatibility.md) for the exact custom endpoint +contract and [`docs/operations.md`](docs/operations.md) for security, backup, recovery, and +troubleshooting guidance. + No external coding-agent CLI or SDK is required. ## Development diff --git a/android/app/build.gradle b/android/app/build.gradle index 4f460709..73d5d718 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,7 @@ apply plugin: 'com.android.application' +import groovy.json.JsonSlurper + android { namespace = "com.opengui.app" compileSdk = rootProject.ext.compileSdkVersion @@ -7,25 +9,27 @@ android { applicationId "com.opengui.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - def resolvedVersionName = System.getenv("GITHUB_REF_NAME") - ?: providers.exec { - commandLine "git", "describe", "--tags", "--abbrev=0" - ignoreExitValue = true - }.standardOutput.asText.get().trim() - ?: "v0.0.1" + def packageVersion = new JsonSlurper().parse(rootProject.file('../package.json')).version + def resolvedVersionName = System.getenv("OPENGUI_VERSION") ?: packageVersion ?: "0.0.1" resolvedVersionName = resolvedVersionName.replaceFirst(/^v/, "") - def semverMatch = resolvedVersionName =~ /^(\d+)\.(\d+)\.(\d+)/ + def semverMatch = resolvedVersionName =~ /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/ if (!semverMatch) { resolvedVersionName = "0.0.1" - semverMatch = resolvedVersionName =~ /^(\d+)\.(\d+)\.(\d+)/ + semverMatch = resolvedVersionName =~ /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/ } def major = semverMatch[0][1].toInteger() def minor = semverMatch[0][2].toInteger() def patch = semverMatch[0][3].toInteger() - versionCode Math.max(1, major * 1000000 + minor * 1000 + patch) + def prerelease = semverMatch[0][4] + def prereleaseNumber = prerelease + ? ((prerelease =~ /(?:^|\.)(\d+)$/).with { it ? it[0][1].toInteger() : 1 }) + : 99 + def releaseSequence = prerelease ? Math.max(1, Math.min(98, prereleaseNumber)) : 99 + + versionCode Math.max(1, (major * 1000000 + minor * 1000 + patch) * 100 + releaseSequence) versionName resolvedVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 693c2e32..df10e2f7 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -1,6 +1,6 @@ // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN include ':capacitor-android' -project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.4.0_@capacitor+core@8.4.0/node_modules/@capacitor/android/capacitor') +project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.4.1_@capacitor+core@8.4.1/node_modules/@capacitor/android/capacitor') include ':capacitor-app' -project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.4.0/node_modules/@capacitor/app/android') +project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.4.1/node_modules/@capacitor/app/android') diff --git a/docker-compose.yml b/docker-compose.yml index d3c0e77f..b50b0738 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,10 +11,20 @@ services: HOST: "${HOST:-127.0.0.1}" PORT: "${PORT:-4839}" OPENGUI_SERVER_MODE: "${OPENGUI_SERVER_MODE:-combined}" + OPENGUI_DATA_DIR: "/app/.opengui-data" + OPENGUI_BASE_URL: "${OPENGUI_BASE_URL:-}" + OPENGUI_AUTH_SECRET: "${OPENGUI_AUTH_SECRET:-}" + OPENGUI_AUTH_TOKEN: "${OPENGUI_AUTH_TOKEN:-}" + OPENGUI_CORS_ORIGIN: "${OPENGUI_CORS_ORIGIN:-}" + OPENGUI_PATH_GRANTS: "${OPENGUI_PATH_GRANTS:-enforced}" + OPENGUI_UPLOAD_MAX_FILE_BYTES: "${OPENGUI_UPLOAD_MAX_FILE_BYTES:-104857600}" + OPENGUI_UPLOAD_MAX_BATCH_BYTES: "${OPENGUI_UPLOAD_MAX_BATCH_BYTES:-209715200}" + OPENGUI_REQUEST_MAX_BYTES: "${OPENGUI_REQUEST_MAX_BYTES:-1048576}" OPENGUI_HOST_EXEC: "1" OPENGUI_HOST_UID: "${OPENGUI_HOST_UID:-1000}" OPENGUI_HOST_GID: "${OPENGUI_HOST_GID:-1000}" OPENGUI_HOST_HOME: "${HOME}" + OPENGUI_HOST_PATH: "${OPENGUI_HOST_PATH:-}" OPENGUI_ALLOWED_ROOTS: "${OPENGUI_ALLOWED_ROOTS:-${HOME}/Code}" ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" OPENAI_API_KEY: "${OPENAI_API_KEY:-}" @@ -23,6 +33,12 @@ services: volumes: - "${HOME}:${HOME}" - opengui-data:/app/.opengui-data + healthcheck: + test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:${PORT:-4839}/api/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s volumes: opengui-data: diff --git a/docs/adr/0014-flexible-users-access-and-model-offerings.md b/docs/adr/0014-flexible-users-access-and-model-offerings.md index 88a80451..23777801 100644 --- a/docs/adr/0014-flexible-users-access-and-model-offerings.md +++ b/docs/adr/0014-flexible-users-access-and-model-offerings.md @@ -6,7 +6,17 @@ This ADR decides the target shape for a flexible **users and access** system—i ## Status -proposed +accepted + +Accepted by implementation on 2026-07-28. The merged vertical slice includes model offerings and +entitlements, Host-side offering resolution, `admin` / `viewer` roles, direct-user collaborative +`run` authorization, backend-oriented settings, and the corresponding integration tests. Evidence: +implementation commit `28ca43c`, the +[`flexible users and model offerings` plan](../plans/flexible-users-access-and-model-offerings.md), +and `packages/backend/src/identity/roles-model-offerings.integration.test.ts`. Later +auth strategies, Host login methods, secret-custody upgrades, and named Teams remain phased work; +they do not reopen the object and authorization boundaries decided here. In particular, this +acceptance does not claim that deferred plan phases 2, 4, or 5 have shipped. ## North star diff --git a/docs/adr/README.md b/docs/adr/README.md index 7f73b829..0fa70e76 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -17,7 +17,8 @@ Read these when code or docs disagree about **who owns what**. | [0011](./0011-host-embedded-accounts-and-teams.md) | **Host-embedded Accounts & Team membership**; invite-only Remote Host auth; API keys; shared Sessions | | [0012](./0012-host-path-grants-and-tool-enforcement.md) | **Host path grants**; canonical roots, restricted actors, complete tool/transport mediation | | [0013](./0013-multi-user-host-access-model.md) | **Multi-user Host access**; user-default shares, session ACL, model planes, registration modes | -| [0014](./0014-flexible-users-access-and-model-offerings.md) | **Flexible users & model offerings** (proposed); backends, credentials, slugs, roles/UI | +| [0014](./0014-flexible-users-access-and-model-offerings.md) | **Flexible users & model offerings** (accepted); backends, credentials, slugs, roles/UI | +| [0015](./0015-host-owned-mcp-tool-connections.md) | **Host-owned MCP tools**; scoped runtimes, authorization, bounded discovery, progressive disclosure | Product glossary (no implementation detail): [`CONTEXT.md`](../../CONTEXT.md). @@ -31,4 +32,4 @@ Host identity / Team access plan: [`docs/plans/host-identity-and-teams.md`](../p Host path policy: [ADR 0012](./0012-host-path-grants-and-tool-enforcement.md). -Flexible users / model offerings (proposed): [ADR 0014](./0014-flexible-users-access-and-model-offerings.md), plan [`flexible-users-access-and-model-offerings.md`](../plans/flexible-users-access-and-model-offerings.md). +Flexible users / model offerings: [ADR 0014](./0014-flexible-users-access-and-model-offerings.md), plan [`flexible-users-access-and-model-offerings.md`](../plans/flexible-users-access-and-model-offerings.md). diff --git a/docs/architecture.md b/docs/architecture.md index a898ef13..2f079963 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,135 +1,91 @@ -# OpenGUI architecture notes - -Contributor map of the repo **as it exists today**. Product language and ownership: [`CONTEXT.md`](../CONTEXT.md). Accepted decisions: [`docs/adr/`](./adr/). - -## Layers (canonical) - -Four layers — same definitions as [`CONTEXT.md` → Architecture](../CONTEXT.md#architecture): - -| Layer | Owns | Does not own | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | -| **OpenGUI Runtime** | Harness Adapters, normalized `HarnessEvent` stream, Harness Inventory, Agent sends on **Harness Scope** (`harnessId` + directory + harness session id). Session/transcript **truth** stays in the Harness. | Queued prompts, multi-Frontend transport, Workspaces, queue UI, `OpenGuiClient` | -| **OpenGUI Backend** | One embedded Runtime per process; HTTP/WebSocket/SSE and Desktop IPC transport; Queued prompts + Queue dispatch; Backend arbitration; Host-embedded Account and API-key authorization; Backend persistence (identity, queue, uploads cleanup). Delegates execution to Runtime. | Workspace connection state, sidebar Project membership, Pending prompts, presentation metadata | -| **OpenGUI Frontend** | Workspaces, Frontend Projects (saved paths), Pending prompts, queue UI, session presentation metadata, UI preferences (via **Frontend persistence**). Talks only to Backend via `OpenGuiClient`. | Harness SDK/CLI, session/transcript source of truth, shared queue storage | -| **Shell** | Bootstrap Frontend (Desktop / Web / Mobile): window chrome, file picker, sidecar lifecycle, static hosting. | Harness execution, session truth, queue dispatch | - -**SDK v1** is in-process only: [`@opengui/runtime`](../packages/runtime/README.md). Target surface: `OpenGUI.create`, `at(directory)`, `SessionHandle` (`send`, `onStream`, `waitUntilIdle`) per [ADR 0007](./adr/0007-runtime-sdk-minimal-surface.md) and [`runtime-sdk-minimal-surface.md`](./plans/runtime-sdk-minimal-surface.md). No queue API in the SDK — use Backend for shared queues ([ADR 0005](./adr/0005-opengui-runtime-backend-split-and-sdk.md)). - -**Harness** = coding-agent CLI/runtime (OpenCode, Claude Code, Codex, Pi). **Provider** = model/API vendor inside a Harness. Never call the OpenGUI server process an “agent backend” ([ADR 0001](./adr/0001-harness-terminology.md)). - -## Where code lives today - -Target layout is in [`plans/runtime-backend-sdk-split.md`](./plans/runtime-backend-sdk-split.md). Current mapping: - -| Layer | Package / entry | Main paths | -| ------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Shared wire types | `@opengui/protocol` | `packages/protocol/src/` (`HarnessId`, `OpenGuiCapabilities`, `QueueMode`, `SelectedModel`) | -| Runtime | `@opengui/runtime` | `packages/runtime/src/` (`host.ts`, `harness-service.ts`, `harness-runtime.ts`, `open-gui.ts`) | -| Harness Adapters | `packages/runtime/src/adapters/` | `*-bridge.ts`, `harness-adapter-kit.ts` | -| Runtime descriptors | Shared with Frontend protocol | `src/agents/` (`backend.ts`, `cli-harness-factory.ts`, `protocol/`) | -| Backend | `@opengui/backend` + thin entry | `packages/backend/src/` (`createBackendHost`, `host/`, `routes/`, `transport/` — SSE, RPC, FS, static, product API); `server/web-server.ts` (~15 lines, `serve()` only); queue/control services in `server/services/*` until a later pass | -| Frontend | React app | `src/` (`App.tsx`, `components/`, `hooks/`, `features/`, `protocol/`) | -| Desktop Shell | Electron | `main.ts`, `preload.ts`, `main/backend-sidecar.ts` | - -**Rule:** UI and hooks call **Backend** APIs only, not bridge IPC. Bridges register inside the Backend process via Runtime ([ADR 0005](./adr/0005-opengui-runtime-backend-split-and-sdk.md)). - -**Repo map maintenance:** Any PR that moves `server/web-server.ts`, `packages/backend/**`, or Harness bridge modules under `packages/runtime/src/adapters/` must update this section (layer table and paths) in the same PR. CI guard: `pnpm run slop-check` (thin `web-server`, no `lib/harness-adapter-kit`). - -**Storage:** [ADR 0004](./adr/0004-storage-source-of-truth-boundaries.md) — Harness owns sessions/transcripts; Backend SQLite owns queues/uploads; Frontend persistence owns Workspaces/Projects/UI. - -**Session reads:** [ADR 0006](./adr/0006-harness-only-session-and-transcript-reads.md), plan [`session-read-slop-removal.md`](./plans/session-read-slop-removal.md), manual [`session-read-acceptance.md`](./manual/session-read-acceptance.md). - -**Desktop transport:** [ADR 0003](./adr/0003-persistent-desktop-backend-transport.md) — Local Workspace uses private IPC, not loopback HTTP. - -**Host identity / Team access:** [ADR 0011](./adr/0011-host-embedded-accounts-and-teams.md), [ADR 0012](./adr/0012-host-path-grants-and-tool-enforcement.md), [ADR 0013](./adr/0013-multi-user-host-access-model.md), plans [`host-identity-and-teams.md`](./plans/host-identity-and-teams.md) and [`multi-user-host-access.md`](./plans/multi-user-host-access.md). Remote Hosts provide Accounts, invites, Team management, revocable Host API keys, durable Actor attribution, and path grants. Multi-user access is **user-default** with explicit shares (paths, models, sessions)—not ambient Team roommate semantics. Desktop Local remains Account-free. Path enforcement is disabled by default; when enabled, member access is deny-by-default and restricted shell is unavailable. Shell is not a grant jail until a later sandbox ADR. - -**Flexible users & model offerings (proposed):** [ADR 0014](./adr/0014-flexible-users-access-and-model-offerings.md), plan [`flexible-users-access-and-model-offerings.md`](./plans/flexible-users-access-and-model-offerings.md). Target split: **Model backend** + **Provider credentials** + user-facing **Model offering** (slug such as “Company Model”); richer Host roles/capabilities and pluggable Host auth methods; UI for backends, offerings, and entitlements. Do not implement against ad-hoc Settings cards alone—follow the plan phases. - -**MCP tools:** [ADR 0015](./adr/0015-host-owned-mcp-tool-connections.md). MCP connections, credentials, transports, catalog budgeting, and actor/Session isolation are Host-owned in `packages/backend/src/mcp/`. The Harness consumes a protocol-neutral `AgentToolSource`; model adapters never import the MCP SDK. Restricted path-policy actors receive no MCP tools. - -**Host path policy:** [ADR 0012](./adr/0012-host-path-grants-and-tool-enforcement.md). `packages/backend/src/path-policy/` and the identity grant schema/routes are foundation only. Do not describe path grants as enforced until HTTP/RPC/SSE, uploads, Session visibility, and every Harness tool consume `IdentityService.effectivePathPolicy(actor)`. Remote multi-user Hosts are **share-only** for paths (no auto user homes); Desktop keeps device default directories. - -Implementation checklists: - -- [`plans/runtime-backend-sdk-split.md`](./plans/runtime-backend-sdk-split.md) — packages / SDK (Phases 1–3 largely done). -- [`plans/contributor-experience-and-slop-removal.md`](./plans/contributor-experience-and-slop-removal.md) — docs, session index, naming, registry, guardrails. -- [`plans/session-read-slop-removal.md`](./plans/session-read-slop-removal.md) — ADR 0006 detail. -- [`plans/host-identity-and-teams.md`](./plans/host-identity-and-teams.md) — Accounts, Team management, attribution, and optional path-grant enforcement (Phases 0–4 done). -- [`plans/multi-user-host-access.md`](./plans/multi-user-host-access.md) — registration modes, canInvite, session ACL, model planes (ADR 0013). -- [`plans/flexible-users-access-and-model-offerings.md`](./plans/flexible-users-access-and-model-offerings.md) — offerings/slugs, backend auth strategies, roles/UI (ADR 0014, proposed). - -Optional CI: `node scripts/slop-check.mjs`. - -## Harness architecture - -Harness-facing modules live in `src/agents/`: - -- `backend.ts` defines the normalized Harness interface and event shapes used by the app. -- `index.ts` defines supported Harness IDs and routing helpers. -- `cli-harness-factory.ts` contains shared local-CLI defaults and `createCliHarnessNormalizer()` for local CLI Harnesses. -- `claude-code.ts`, `codex.ts`, and `pi.ts` are small descriptors built from the CLI factory. -- `opencode.ts` declares OpenCode capabilities/workspace shape and delegates SDK event translation to `src/agents/protocol/opencode-map.ts`. -- `id-codec.ts` is the Harness ID-codec seam. `shared.ts` re-exports it and keeps session/message tagging helpers. - -When adding or changing a Harness, keep protocol-specific mapping out of UI code. Normalize native events into `HarnessEvent` as close to the adapter as possible, then let the rest of the app consume the normalized event stream. - -## Adding a Harness - -A **Harness Adapter** is a bridge (`setupXBridge`) plus registry metadata. Start with [`docs/harness-bridge-contract.md`](./harness-bridge-contract.md) and `node scripts/scaffold-harness.mjs `. - -1. [`src/agents/harness-registry.ts`](../src/agents/harness-registry.ts) + [`harness-ids.ts`](../src/agents/harness-ids.ts) — id, label, CLI command. -2. [`cli-harness-factory.ts`](../src/agents/cli-harness-factory.ts) — `HARNESS_BACKEND_META`, `normalizeEvent`. -3. [`harness-bridge-registrations.ts`](../packages/runtime/src/harness-bridge-registrations.ts) — register bridge in `BRIDGE_SETUP_BY_HARNESS_ID`. -4. New `*-bridge.ts` under `packages/runtime/src/adapters/`. -5. [`server/harness-inventory.ts`](../server/harness-inventory.ts) — uses registry CLI map. -6. [`session-identity.ts`](../src/lib/session-identity.ts) — parse legacy ids only; new ids via `composeFrontendSessionId`. - -Descriptors in `src/agents/.ts`: use `makeLocalCliCapabilities()` and `createCliHarnessNormalizer()` for tagged CLI streams; custom SDK events go in `src/agents/protocol/` with tests. Session IDs: `composeFrontendSessionId` / codecs in `src/agents/shared.ts`, not ad-hoc strings. - -## Frontend feature slices - -The current frontend is still centered on `src/App.tsx`, but several orchestration concerns have been moved into `src/features/`: - -- `features/app-shell/useAppKeyboardShortcuts.ts` owns app-level keyboard shortcut orchestration. -- `features/session/useActiveSessionQueue.ts` owns active-session queue UI handlers. -- `features/session/useChatSessionSurface.ts` derives the active chat surface state. -- `features/worktree/useActiveWorktreeMerge.ts` owns active worktree merge and pull-request actions. -- `features/local-intent/` owns **Local intent orchestration** (Pending prompt → Agent send, Queued prompt dispatch from PromptBox). `HarnessProvider` (`use-agent-impl-core.tsx`) wires React state and delegates `sendPrompt` / `sendCommand` / queue side effects through `useLocalIntentOrchestration`. -- `features/agent-bootstrap/` — workspace persistence load + post-ready project/server bootstrap. -- `features/agent-resources/` — `loadServerResources` / resource catalog dedupe (`useAgentResourceCatalog`). - -New UI orchestration should follow this direction: keep reusable visual pieces in `src/components/`, keep cross-component state orchestration in a named `src/features//` hook, and keep pure domain utilities in `src/lib/`. - -## Shared UI primitives - -Reusable UI building blocks live in `src/components/ui/`. Recent dialog work uses: - -- `DialogShell` for common dialog layout and footer/body structure. -- `DialogHeader` for consistent dialog titles, descriptions, and icons. -- `ButtonGroup`, `FormField`, and `ToggleSwitch` for repeated form/action patterns. - -Prefer these primitives before adding another one-off dialog header, footer, button group, or toggle implementation. - -## Provider icons - -Provider icons are resolved by `src/components/provider-icons/ProviderIcon.tsx` and `types.ts`. Vite expands the SVG asset manifest from `src/components/provider-icons/svgs/*.svg` with `import.meta.glob`, so adding an icon should only require dropping in a correctly named SVG unless new fallback or alias behavior is needed. - -## Commands +# OpenGUI architecture + +Contributor map of the repository as it exists for the 0.6 release line. Canonical product +language is in [`CONTEXT.md`](../CONTEXT.md); accepted decisions are indexed in +[`docs/adr/`](./adr/README.md). + +## Runtime shape + +```text +Desktop Shell ─┐ +Web Shell ─────┼─ OpenGUI Frontend ── authenticated Host API/events ── OpenGUI Host +Mobile Shell ──┘ ├─ identity + authorization + ├─ model/provider credentials + ├─ MCP connections + └─ first-party Harness + ├─ Session SQLite + ├─ model adapters + └─ built-in + MCP tools +``` -Vite+ (`vp`) is a dev dependency. After `pnpm install`, use **`pnpm vp …`** or **`pnpm run