Skip to content

Release v1.0.0-beta.4: install OOM fix + reduce-effects + self-hosted CI#1206

Merged
jaylfc merged 17 commits into
masterfrom
dev
Jun 20, 2026
Merged

Release v1.0.0-beta.4: install OOM fix + reduce-effects + self-hosted CI#1206
jaylfc merged 17 commits into
masterfrom
dev

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Promotes this session's work to master and cuts beta.4.

User-facing

Infra

NOTE: the first master CI run after merge publishes the rolling bundle-latest prebuilt bundle for the first time -- watching it. The beta.4 GitHub release is cut from master tip after that's verified.

jaylfc added 16 commits June 20, 2026 12:38
The slow python suite (~16min on GitHub's shared runners) now runs on two
self-hosted runners labelled taos-ci: the always-on VPS (primary) and the
Fedora box (secondary + GPU). Sharing the label lets the 3.12/3.13 matrix
run in parallel and gives redundancy if one box is offline. lint and
spa-build stay on ubuntu-latest as a free, always-available fallback.

This PR self-validates: its own test job runs on the new runners.
actions/setup-python only ships prebuilt CPython for GitHub's ubuntu image,
so it fails on the self-hosted Fedora runner ('version 3.12 not found for
Fedora 43'). Drop it and let uv provision Python from its distro-agnostic
standalone builds, which works on Fedora, the Ubuntu VPS, and GitHub-hosted.

Also add a pick-runner job that selects the highest-priority available tier:
the always-on VPS first, then Fedora, then ubuntu-latest as a never-hang
fallback (needs secret RUNNER_ADMIN_PAT with administration:read to read
runner status; without it, defaults to the self-hosted pool).
…oth down

Benchmarked the two boxes: they are ~tied per core (Fedora i5-10600 ~2.0s,
VPS EPYC-Milan ~2.2s on the same single-thread microbench). So the fastest
run is the two matrix legs in parallel across both boxes, not pinning the
matrix to one 'primary' runner (which would serialise the legs). Route to the
taos-ci pool and only fall back to ubuntu-latest when both self-hosted runners
are offline.
ci: run test job on self-hosted runners (VPS + Fedora)
Adds a vitest run to the spa-build job so the ~1,900 desktop tests are gated
on every PR (they were never run in CI before). To make that green:

- Fix two stale tests: userspace app ids are namespaced 'userspace:<id>' (#89),
  and the projectsApi.subscribeEvents mock must be constructable (the code uses
  'new EventSource', and an arrow function can't be a constructor).
- Quarantine 9 suites in vite.config.ts test.exclude: the AgentsApp (#59) and
  Browser/AddressBar (#66) suites drift against those in-progress redesigns, and
  EmojiPicker is order-dependent (passes alone, fails under the full suite).
  Each is tagged #114 to un-exclude as its owning work lands.

223 files / 1829 tests pass locally; 0 failures.
ci: gate the desktop vitest suite (#114)
Adds an opt-in Reduce effects toggle (Settings -> Accessibility) that strips the
GPU-heavy compositing which tanks framerate on weak hardware: the ~90 backdrop
blur surfaces, the large soft shadows, and the continuously-running animations.

A reduceEffects flag in the theme store persists the choice and App.tsx applies
a data-perf=reduced attribute on the root; tokens.css keys the strip-down off
that attribute, so layout, colour and contrast are untouched and the default
(effects on) path is byte-for-byte unchanged. Surfaces that leaned on the blur
get an opaque glass background so they stay legible.

Reported by a user on an older laptop (GTX 1060, Edge) seeing UI lag. Auto-enable
on detected low-end GPUs is a planned fast-follow.
feat(perf): reduce-effects mode for low-end devices (#58)
…ally (#117)

End users were building the SPA locally on every install/upgrade (the bundle is
gitignored, releases shipped no prebuilt asset). The vite build needs ~2-4GB and
OOMs on small machines (e.g. an 8GB WSL); install-server.sh did 'npm run build
|| die' AFTER the git source update, so a killed build left the install silently
serving the OLD UI -- exactly the 'ran the installer, still on the old version'
report.

- CI (spa-build, master only) publishes the freshly-built static/desktop as a
  rolling 'bundle-latest' prerelease, keyed by the git tree SHA of desktop/ so
  the bundle stays valid across every commit that doesn't touch the frontend.
- install-server.sh downloads + stage-swaps that bundle when the tree SHA
  matches, skipping the local build entirely. Local build is now the fallback
  only, and it fails LOUDLY (memory cause + the .wslconfig fix + 'your UI was
  NOT updated') instead of half-updating.

Activates once this reaches master (the publish step + the curl-fetched
installer both read master). In-app update path (desktop_rebuild.py) gets the
same prebuilt preference as a fast-follow.
feat(install): prebuilt SPA bundle, no local build on install (#117)
The in-app / auto update path (desktop_rebuild.py) still ran npm run build
locally, so it would OOM on the same small machines the install script now
spares. Mirror the install-server.sh logic: before the local build, try the
CI-published prebuilt bundle (keyed by the git tree SHA of desktop/), download
+ stage + swap it into static/desktop/, and skip npm entirely on a match. Tar
is extracted with the path-safe data filter. Falls back to the local build on
any mismatch, missing git, or network/extract failure.

Tests: 3 new cases (match installs the bundle, mismatch never downloads it,
missing git falls back); reworked the build-failure test to dispatch by args
since the prebuilt check now makes a git call first. 21 pass.
feat(update): in-app update prefers the prebuilt bundle (#117)
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@jaylfc, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 8 minutes and 28 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b99f7b7-2b3d-4cc0-a753-7840bf784c59

📥 Commits

Reviewing files that changed from the base of the PR and between 8996ac2 and 96eb555.

📒 Files selected for processing (14)
  • .github/workflows/ci.yml
  • desktop/src/App.tsx
  • desktop/src/apps/SettingsApp.tsx
  • desktop/src/lib/__tests__/projects-tasks.test.ts
  • desktop/src/lib/__tests__/userspace-apps.test.ts
  • desktop/src/stores/__tests__/reduce-effects.test.ts
  • desktop/src/stores/theme-store.ts
  • desktop/src/theme/tokens.css
  • desktop/vite.config.ts
  • docs/STATUS.md
  • scripts/install-server.sh
  • tests/test_desktop_rebuild.py
  • tinyagentos/__init__.py
  • tinyagentos/desktop_rebuild.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

👋 Thanks for the PR! This one targets master, which is our
stable branch (it's what live installs track). Please retarget it to
dev — click Edit next to the PR title and change the base
branch dropdown from master to dev. Your commits and any review
carry over, nothing is lost.

See CONTRIBUTING.md for the branch model.

So each published version (e.g. v1.0.0-beta.4) is a self-contained packaged
download carrying its own desktop-bundle.tar.gz + desktop-tree.txt, in addition
to the rolling bundle-latest the installer fetches by default.
@gitar-bot

gitar-bot Bot commented Jun 20, 2026

Copy link
Copy Markdown

Note

Your trial team has used its Gitar budget, so automatic reviews are paused. Upgrade now to unlock full capacity. Comment "Gitar review" to trigger a review manually.
Learn more about usage limits

Code Review 👍 Approved with suggestions 0 resolved / 5 findings

Introduces prebuilt SPA bundles and a low-end device performance mode to improve installation reliability. Address several security and robustness concerns regarding prebuilt bundle integrity, insecure temporary file paths, and potential race conditions during the installation swap.

💡 Security: Prebuilt bundle installed with no content integrity verification

📄 tinyagentos/desktop_rebuild.py:101-115 📄 scripts/install-server.sh:1170-1184 📄 .github/workflows/ci.yml:152-166

_try_prebuilt_desktop_bundle (and the mirrored logic in install-server.sh) downloads desktop-bundle.tar.gz from the public bundle-latest release and serves its contents as the desktop UI after only checking that desktop-tree.txt equals the local git rev-parse HEAD:desktop. But desktop-tree.txt is published in the same release asset set as the tarball, so it provides no integrity guarantee — anyone who can write to the rolling release (or any future asset-tampering) can ship a malicious bundle alongside a matching tree string and it will be extracted and served verbatim. The 'it's our own CI output' assumption in the docstring rests entirely on GitHub release/account security, with no independent check.

Suggested hardening: after extraction, recompute a git-tree-style hash of the staged desktop/ (or, more simply, publish and verify a SHA-256 of the tarball that is generated independently of the tree key) before swapping it into static/desktop/. At minimum, document that the trust boundary is the GitHub release.

💡 Security: tar extracted without path-safe filter on older Python

📄 tinyagentos/desktop_rebuild.py:152-156 🔗 CWE-22

In _try_prebuilt_desktop_bundle, when tar.extractall(stage, filter="data") raises TypeError (Python without the filter kwarg) the code falls back to tar.extractall(stage) with no filtering. That fallback is vulnerable to path traversal / absolute-path / symlink entries (CVE-2007-4559 class) if the archive is ever malicious — which compounds the lack of integrity verification above. The filter="data" kwarg only landed in 3.12 (and 3.11.4+/3.10.12+ backports), so installs on older patch levels silently take the unsafe path.

Suggested fix: on the TypeError branch, either refuse to extract (return False and fall back to a local build) or manually validate each member's resolved path stays within stage before extracting.

Fall back to local build instead of unsafe extraction on old Python.
with tarfile.open(fileobj=io.BytesIO(blob), mode="r:gz") as tar:
    try:
        tar.extractall(stage, filter="data")  # py>=3.12 path-safe filter
    except TypeError:
        # No data filter on this Python: refuse rather than extract
        # an unvalidated archive (path-traversal risk). Build locally.
        logger.warning("tar 'data' filter unavailable; building locally.")
        return False
💡 Edge Case: Prebuilt helper errors abort rebuild instead of falling back to build

📄 tinyagentos/desktop_rebuild.py:112-122

The docstring states _try_prebuilt_desktop_bundle returns False (falling back to a local build) on "any mismatch, missing git, or network/extract failure." However the git-probe block only catches FileNotFoundError. If asyncio.create_subprocess_exec("git", ...) or proc.communicate() raises any other exception (e.g. OSError/PermissionError when spawning git), it propagates out of the helper to the outer except Exception in rebuild_desktop_bundle_if_stale, which returns RebuildResult(rebuilt=True, success=False). That reports the whole rebuild as failed and never attempts the local build — the opposite of the intended graceful fallback.

Suggested fix: broaden the probe's exception handling to return False on any exception, so the prebuilt path can never harden into a failure that blocks the local build.

Catch any exception from the git probe and fall back.
try:
    proc = await asyncio.create_subprocess_exec(
        "git", "-C", str(project_root), "rev-parse", "HEAD:desktop",
        stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
    )
    out, _ = await proc.communicate()
except Exception:
    return False  # git missing or unspawnable -> fall back to local build
💡 Security: install-server.sh uses predictable /tmp paths during root install

📄 scripts/install-server.sh:1177-1191

The installer stages the prebuilt bundle in fixed, world-predictable paths. Since the installer commonly runs as root, on a multi-user host a local attacker can pre-create these as symlinks or pre-seed files: removing the symlink itself, but the subsequent directory creation, download, and extraction operate on attacker-influenceable predictable names, enabling symlink/redirection style attacks. Use a randomized directory instead (e.g. mktemp -d) and extract/download inside it.

Replace fixed /tmp paths with mktemp-created locations.
_stage="$(mktemp -d)"
_tarball="$(mktemp)"
if curl -fsSL --max-time 120 "$_bundle_base/desktop-bundle.tar.gz" -o "$_tarball" \
   && tar -C "$_stage" -xzf "$_tarball" \
   && [[ -f "$_stage/desktop/index.html" ]]; then
    rm -rf "$INSTALL_DIR/static/desktop"
    mkdir -p "$INSTALL_DIR/static"
    mv "$_stage/desktop" "$INSTALL_DIR/static/desktop"
    # ... (chown unchanged) ...
fi
rm -rf "$_stage" "$_tarball"
💡 Bug: Prebuilt install deletes existing bundle before move can fail

📄 tinyagentos/desktop_rebuild.py:161-171

In _try_prebuilt_desktop_bundle the swap does shutil.rmtree(target) (removing the existing, working static/desktop/) and only then shutil.move(staged, target). stage is created under the system temp dir, which is frequently a different filesystem from the project, so shutil.move degrades to copy-then-delete and can fail partway (disk full, permissions, etc.). If the move fails after the rmtree, the install is left with no desktop bundle at all — the very 'silently stuck with no UI' outcome this feature was meant to prevent. The verify-before-swap (index.html check) guards against bad archives but not against a failed move. Consider moving the staged tree into a sibling of target (same filesystem) and doing an atomic-ish rename, or copy-into-temp-then-rename, so the old bundle survives a failed swap.

🤖 Prompt for agents
Code Review: Introduces prebuilt SPA bundles and a low-end device performance mode to improve installation reliability. Address several security and robustness concerns regarding prebuilt bundle integrity, insecure temporary file paths, and potential race conditions during the installation swap.

1. 💡 Security: Prebuilt bundle installed with no content integrity verification
   Files: tinyagentos/desktop_rebuild.py:101-115, scripts/install-server.sh:1170-1184, .github/workflows/ci.yml:152-166

   `_try_prebuilt_desktop_bundle` (and the mirrored logic in install-server.sh) downloads `desktop-bundle.tar.gz` from the public `bundle-latest` release and serves its contents as the desktop UI after only checking that `desktop-tree.txt` equals the local `git rev-parse HEAD:desktop`. But `desktop-tree.txt` is published in the *same* release asset set as the tarball, so it provides no integrity guarantee — anyone who can write to the rolling release (or any future asset-tampering) can ship a malicious bundle alongside a matching tree string and it will be extracted and served verbatim. The 'it's our own CI output' assumption in the docstring rests entirely on GitHub release/account security, with no independent check.
   
   Suggested hardening: after extraction, recompute a git-tree-style hash of the staged `desktop/` (or, more simply, publish and verify a SHA-256 of the tarball that is generated independently of the tree key) before swapping it into `static/desktop/`. At minimum, document that the trust boundary is the GitHub release.

2. 💡 Security: tar extracted without path-safe filter on older Python
   Files: tinyagentos/desktop_rebuild.py:152-156

   In `_try_prebuilt_desktop_bundle`, when `tar.extractall(stage, filter="data")` raises `TypeError` (Python without the `filter` kwarg) the code falls back to `tar.extractall(stage)` with no filtering. That fallback is vulnerable to path traversal / absolute-path / symlink entries (CVE-2007-4559 class) if the archive is ever malicious — which compounds the lack of integrity verification above. The `filter="data"` kwarg only landed in 3.12 (and 3.11.4+/3.10.12+ backports), so installs on older patch levels silently take the unsafe path.
   
   Suggested fix: on the `TypeError` branch, either refuse to extract (return False and fall back to a local build) or manually validate each member's resolved path stays within `stage` before extracting.

   Fix (Fall back to local build instead of unsafe extraction on old Python.):
   with tarfile.open(fileobj=io.BytesIO(blob), mode="r:gz") as tar:
       try:
           tar.extractall(stage, filter="data")  # py>=3.12 path-safe filter
       except TypeError:
           # No data filter on this Python: refuse rather than extract
           # an unvalidated archive (path-traversal risk). Build locally.
           logger.warning("tar 'data' filter unavailable; building locally.")
           return False

3. 💡 Edge Case: Prebuilt helper errors abort rebuild instead of falling back to build
   Files: tinyagentos/desktop_rebuild.py:112-122

   The docstring states `_try_prebuilt_desktop_bundle` returns False (falling back to a local build) on "any mismatch, missing git, or network/extract failure." However the git-probe block only catches `FileNotFoundError`. If `asyncio.create_subprocess_exec("git", ...)` or `proc.communicate()` raises any other exception (e.g. `OSError`/`PermissionError` when spawning git), it propagates out of the helper to the outer `except Exception` in `rebuild_desktop_bundle_if_stale`, which returns `RebuildResult(rebuilt=True, success=False)`. That reports the whole rebuild as failed and never attempts the local build — the opposite of the intended graceful fallback.
   
   Suggested fix: broaden the probe's exception handling to return False on any exception, so the prebuilt path can never harden into a failure that blocks the local build.

   Fix (Catch any exception from the git probe and fall back.):
   try:
       proc = await asyncio.create_subprocess_exec(
           "git", "-C", str(project_root), "rev-parse", "HEAD:desktop",
           stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
       )
       out, _ = await proc.communicate()
   except Exception:
       return False  # git missing or unspawnable -> fall back to local build

4. 💡 Security: install-server.sh uses predictable /tmp paths during root install
   Files: scripts/install-server.sh:1177-1191

   The installer stages the prebuilt bundle in fixed, world-predictable paths. Since the installer commonly runs as root, on a multi-user host a local attacker can pre-create these as symlinks or pre-seed files: removing the symlink itself, but the subsequent directory creation, download, and extraction operate on attacker-influenceable predictable names, enabling symlink/redirection style attacks. Use a randomized directory instead (e.g. `mktemp -d`) and extract/download inside it.

   Fix (Replace fixed /tmp paths with mktemp-created locations.):
   _stage="$(mktemp -d)"
   _tarball="$(mktemp)"
   if curl -fsSL --max-time 120 "$_bundle_base/desktop-bundle.tar.gz" -o "$_tarball" \
      && tar -C "$_stage" -xzf "$_tarball" \
      && [[ -f "$_stage/desktop/index.html" ]]; then
       rm -rf "$INSTALL_DIR/static/desktop"
       mkdir -p "$INSTALL_DIR/static"
       mv "$_stage/desktop" "$INSTALL_DIR/static/desktop"
       # ... (chown unchanged) ...
   fi
   rm -rf "$_stage" "$_tarball"

5. 💡 Bug: Prebuilt install deletes existing bundle before move can fail
   Files: tinyagentos/desktop_rebuild.py:161-171

   In `_try_prebuilt_desktop_bundle` the swap does `shutil.rmtree(target)` (removing the existing, working `static/desktop/`) and only then `shutil.move(staged, target)`. `stage` is created under the system temp dir, which is frequently a different filesystem from the project, so `shutil.move` degrades to copy-then-delete and can fail partway (disk full, permissions, etc.). If the move fails after the rmtree, the install is left with no desktop bundle at all — the very 'silently stuck with no UI' outcome this feature was meant to prevent. The verify-before-swap (index.html check) guards against bad archives but not against a failed move. Consider moving the staged tree into a sibling of `target` (same filesystem) and doing an atomic-ish rename, or copy-into-temp-then-rename, so the old bundle survives a failed swap.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Important

Your trial ends in 7 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

@jaylfc jaylfc merged commit 010e49b into master Jun 20, 2026
12 of 13 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in TinyAgentOS Roadmap Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant