Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,20 +161,22 @@ jobs:
tree=$(git rev-parse HEAD:desktop)
tar -C static -czf desktop-bundle.tar.gz desktop
printf '%s\n' "$tree" > desktop-tree.txt
notes="Auto-built SPA bundle, matching desktop/ tree ${tree}. The installer downloads this when the tree matches, skipping the local vite build."
# Integrity digest the installer verifies before extracting.
sha256sum desktop-bundle.tar.gz | awk '{print $1}' > desktop-bundle.sha256
notes="Auto-built SPA bundle, matching desktop/ tree ${tree}. The installer downloads this when the tree matches, verifies desktop-bundle.sha256, then skips the local vite build."
# Always keep the rolling 'bundle-latest' current (this is what the
# installer fetches by default).
if gh release view bundle-latest >/dev/null 2>&1; then
gh release upload bundle-latest desktop-bundle.tar.gz desktop-tree.txt --clobber
gh release upload bundle-latest desktop-bundle.tar.gz desktop-tree.txt desktop-bundle.sha256 --clobber
gh release edit bundle-latest --notes "$notes"
else
gh release create bundle-latest --prerelease \
--title "Prebuilt desktop bundle (rolling)" --notes "$notes" \
desktop-bundle.tar.gz desktop-tree.txt
desktop-bundle.tar.gz desktop-tree.txt desktop-bundle.sha256
fi
# On a version release, also attach the bundle to that release tag so
# each published version is a self-contained packaged download.
if [ "${{ github.event_name }}" = "release" ]; then
gh release upload "${{ github.event.release.tag_name }}" \
desktop-bundle.tar.gz desktop-tree.txt --clobber
desktop-bundle.tar.gz desktop-tree.txt desktop-bundle.sha256 --clobber
fi
30 changes: 14 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

> **Beta (2026-06-02).** This is beta software meant for testers running it on their own hardware, so expect rough edges. The install script, backend, API, memory system (taOSmd), and multi-framework group chat all work; the desktop GUI is wired up for everyday use but a few flows (some agent management, worker connections, model routing) are still being smoothed out. Star or watch the repo to follow progress and catch the next release.
>
> **A heads-up on the catalogs:** with 100+ apps, 16 frameworks, and a large model catalog, plenty of install manifests have not been exercised on real hardware yet, so some apps, frameworks, and models will fail to install. If one does, [open an issue](https://github.com/jaylfc/tinyagentos/issues) with the name and the error you saw and I will fix the manifest as soon as I can. These reports are genuinely useful, most manifest fixes ship same-day.
> **A heads-up on the catalogs:** with 100+ apps, 16 frameworks, and a large model catalog, plenty of install manifests have not been exercised on real hardware yet, so some apps, frameworks, and models will fail to install. If one does, [open an issue](https://github.com/jaylfc/taOS/issues) with the name and the error you saw and I will fix the manifest as soon as I can. These reports are genuinely useful, most manifest fixes ship same-day.

<p align="center">
<a href="https://www.star-history.com/?repos=jaylfc%2Ftinyagentos&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=jaylfc/tinyagentos&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=jaylfc/tinyagentos&type=date&legend=top-left" />
<img alt="Star History Chart" width="600" src="https://api.star-history.com/chart?repos=jaylfc/tinyagentos&type=date&legend=top-left" />
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=jaylfc/taOS&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=jaylfc/taOS&type=date&legend=top-left" />
<img alt="Star History Chart" width="600" src="https://api.star-history.com/chart?repos=jaylfc/taOS&type=date&legend=top-left" />
</picture>
</a>
</p>
Expand Down Expand Up @@ -73,7 +73,7 @@ Sovereignty by default, cloud by choice. Run taOS fully offline, or connect a cl

```bash
# Debian / Ubuntu / Fedora / Arch / Alpine / macOS, one-line install
curl -fsSL https://raw.githubusercontent.com/jaylfc/tinyagentos/master/scripts/install-server.sh | sudo bash
curl -fsSL https://raw.githubusercontent.com/jaylfc/taOS/master/scripts/install-server.sh | sudo bash
```

Run without `sudo` to install as a user-mode systemd unit instead. The script is idempotent, safe to re-run on an existing install. Supports env-var overrides for install path, branch, and port.
Expand Down Expand Up @@ -157,21 +157,21 @@ Combine ANY device into one AI compute mesh, desktops, laptops, SBCs, even phone
# Linux / macOS, one-line worker install (auto-detects headless,
# installs as a system service when run with sudo or as a user
# service otherwise; works on a fresh Debian install or your existing box)
curl -fsSL https://raw.githubusercontent.com/jaylfc/tinyagentos/master/scripts/install-worker.sh | sudo bash -s -- http://your-server:6969
curl -fsSL https://raw.githubusercontent.com/jaylfc/taOS/master/scripts/install-worker.sh | sudo bash -s -- http://your-server:6969

# Desktop, system tray worker app (interactive, with GUI tray icon)
tinyagentos-worker http://your-server:6969

# Android, one-line Termux setup
curl -sL https://raw.githubusercontent.com/jaylfc/tinyagentos/master/tinyagentos/worker/android_setup.sh | bash
curl -sL https://raw.githubusercontent.com/jaylfc/taOS/master/tinyagentos/worker/android_setup.sh | bash
```

```powershell
# Windows 10/11, one-line worker install (PowerShell, mirrors the
# Linux/macOS installer -- registers a Scheduled Task so the worker
# starts at boot and survives logout)
$env:TAOS_CONTROLLER_URL = 'http://your-server:6969'
iwr -useb https://raw.githubusercontent.com/jaylfc/tinyagentos/master/scripts/install-worker.ps1 | iex
iwr -useb https://raw.githubusercontent.com/jaylfc/taOS/master/scripts/install-worker.ps1 | iex
```

**Pairing.** A worker no longer registers automatically just by reaching the controller. On first run it prints a short pairing code and announces itself as pending; you approve it in taOS (Cluster) by entering that code, which mints the worker's signing key. From then on the worker signs its register and heartbeat calls with that key, so a host on the LAN cannot register or impersonate a worker it does not physically control. Re-run the installer to resume pairing if you do not approve it straight away.
Expand Down Expand Up @@ -432,7 +432,7 @@ Full transparency on every file, service, user, and port the installers touch. N

### Controller install (`scripts/install-server.sh`)

Run `curl -fsSL https://raw.githubusercontent.com/jaylfc/tinyagentos/master/scripts/install-server.sh | sudo bash` on a fresh Debian / Ubuntu / Fedora / Arch / Alpine box to get the controller fully installed, repo cloned to `~/tinyagentos/`, venv created, all deps installed, and both `tinyagentos.service` (port 6969) and `qmd.service` (port 7832) registered and started.
Run `curl -fsSL https://raw.githubusercontent.com/jaylfc/taOS/master/scripts/install-server.sh | sudo bash` on a fresh Debian / Ubuntu / Fedora / Arch / Alpine box to get the controller fully installed, repo cloned to `~/tinyagentos/`, venv created, all deps installed, and both `tinyagentos.service` (port 6969) and `qmd.service` (port 7832) registered and started.

| Where | What |
|---|---|
Expand Down Expand Up @@ -511,12 +511,10 @@ git pull
# Clear stale Python bytecode after upgrades (git pull preserves source mtimes
# which can confuse Python's .pyc cache invalidation on some setups)
find . -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null || true
# Rebuild frontend if desktop source changed (omit if you didn't pull any desktop/ changes)
cd desktop && npm install && npm run build && cd ..
sudo systemctl restart tinyagentos
```

The systemd unit also runs a conditional rebuild as an `ExecStartPre` step -- if you skip the manual `npm run build`, the next service restart detects the stale bundle and rebuilds it automatically (~50s startup overhead when it fires).
You do not build the UI on every machine. `install-server.sh` and the in-app update download a prebuilt SPA bundle published by CI, matched to your `desktop/` source by its git tree hash, so installs and upgrades are fast and never run the memory-heavy vite build (which used to OOM on small machines like an 8GB WSL). The restart above triggers the same conditional fetch via the unit's `ExecStartPre` step when the desktop source changed. A local build (`cd desktop && npm install && npm run build`) is only needed for local frontend development, or kicks in automatically as a fallback when no matching prebuilt bundle is available.

**Worker:**

Expand Down Expand Up @@ -547,7 +545,7 @@ On RK3588 boards with CPU image generation enabled, the sd-cpp backend ships as
`scripts/install-rknpu.sh` is an opt-in automated installer for the full Rockchip NPU stack. It pins `librknnrt` to 2.3.0, installs the jaylfc fork of rkllama, and preloads three chat models. All binaries are fetched from `huggingface.co/jaysom/tinyagentos-rockchip-mirror`, a TAOS-controlled mirror, and SHA256-verified before installation. If any checksum fails the script hard-aborts.

```bash
curl -fsSL https://raw.githubusercontent.com/jaylfc/tinyagentos/master/scripts/install-rknpu.sh | sudo bash
curl -fsSL https://raw.githubusercontent.com/jaylfc/taOS/master/scripts/install-rknpu.sh | sudo bash
```

See [docs/mirror-policy.md](docs/mirror-policy.md) for the mirror governance policy, what is mirrored, when it updates, how to verify integrity independently, and how to self-host the mirror for air-gapped deployments. The same policy will extend to RK3576, Raspberry Pi 4, Mac mini / Apple Silicon, and x86 classes as those verified install paths land.
Expand Down Expand Up @@ -723,7 +721,7 @@ CI runs automatically on every push (Python 3.12 and 3.13 on every PR; Python 3.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines. Join [GitHub Discussions](https://github.com/jaylfc/tinyagentos/discussions) for questions and ideas.
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines. Join [GitHub Discussions](https://github.com/jaylfc/taOS/discussions) for questions and ideas.

## Support the Project

Expand All @@ -748,8 +746,8 @@ If you maintain one of the libraries above and want a different phrasing or a li

taOS is better for the people testing it, filing issues, and sending fixes:

- [@hognek](https://github.com/hognek) -- the first community code contributions: Hermes bridge bounded-retry + dedup ([#468](https://github.com/jaylfc/tinyagentos/pull/468)), agent button states + feedback ([#469](https://github.com/jaylfc/tinyagentos/pull/469)), and browser/PWA mobile layout ([#470](https://github.com/jaylfc/tinyagentos/pull/470)).
- [@johny-mnemonic](https://github.com/johny-mnemonic) -- first to run taOS on a heterogeneous multi-GPU stack beyond our own hardware, surfacing the gaps behind the agent-deploy and UI fixes (the [#357](https://github.com/jaylfc/tinyagentos/discussions/357) thread).
- [@hognek](https://github.com/hognek) -- the first community code contributions: Hermes bridge bounded-retry + dedup ([#468](https://github.com/jaylfc/taOS/pull/468)), agent button states + feedback ([#469](https://github.com/jaylfc/taOS/pull/469)), and browser/PWA mobile layout ([#470](https://github.com/jaylfc/taOS/pull/470)).
- [@johny-mnemonic](https://github.com/johny-mnemonic) -- first to run taOS on a heterogeneous multi-GPU stack beyond our own hardware, surfacing the gaps behind the agent-deploy and UI fixes (the [#357](https://github.com/jaylfc/taOS/discussions/357) thread).
- [@m13v](https://github.com/m13v) and [@redkjuegos](https://github.com/redkjuegos) -- sustained feedback and discussion across issues.
- …and everyone who's opened an issue or tested an early build. 🙏

Expand Down
45 changes: 33 additions & 12 deletions scripts/install-server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,17 @@
# dir = force directory-backed pool (no CoW, slower clones)
set -euo pipefail

INSTALL_DIR="${TAOS_INSTALL_DIR:-$HOME/tinyagentos}"
# If taOS is already installed, default to ITS directory so a re-run updates the
# existing install in place rather than forking a second copy (e.g. a root
# `curl | sudo bash` landing in /root while the service runs from /opt). An
# explicit TAOS_INSTALL_DIR always wins; otherwise prefer the running service's
# WorkingDirectory, then a /opt install, then the invoking user's home.
_existing_install=""
if command -v systemctl >/dev/null 2>&1; then
_existing_install="$(systemctl show tinyagentos -p WorkingDirectory --value 2>/dev/null || true)"
fi
[[ -z "$_existing_install" && -d /opt/tinyagentos/.git ]] && _existing_install=/opt/tinyagentos
INSTALL_DIR="${TAOS_INSTALL_DIR:-${_existing_install:-$HOME/tinyagentos}}"
BRANCH="${TAOS_BRANCH:-master}"
REPO="${TAOS_REPO:-https://github.com/jaylfc/tinyagentos}"
TAOS_PORT="${TAOS_PORT:-6969}"
Expand Down Expand Up @@ -1174,27 +1184,38 @@ if [[ -n "$_desktop_tree" ]]; then
_remote_tree="$(curl -fsSL --max-time 20 "$_bundle_base/desktop-tree.txt" 2>/dev/null | tr -d '[:space:]')"
if [[ -n "$_remote_tree" && "$_remote_tree" == "$_desktop_tree" ]]; then
log "fetching prebuilt desktop bundle (matches source; no local build needed)"
rm -rf /tmp/taos-bundle-stage && mkdir -p /tmp/taos-bundle-stage
# Stage + verify in /tmp first, then swap, so a failed download/extract
# never leaves the install with no UI.
if curl -fsSL --max-time 120 "$_bundle_base/desktop-bundle.tar.gz" -o /tmp/taos-desktop-bundle.tar.gz \
&& tar -C /tmp/taos-bundle-stage -xzf /tmp/taos-desktop-bundle.tar.gz \
&& [[ -f /tmp/taos-bundle-stage/desktop/index.html ]]; then
mkdir -p "$INSTALL_DIR/static"
# Stage INSIDE the install tree (private, same filesystem) rather than a
# fixed world-writable /tmp path: this makes the final swap an atomic
# rename and avoids writing through an attacker-planted symlink while
# running as root in the common `curl | sudo bash` invocation.
_stage="$(mktemp -d "$INSTALL_DIR/static/.taos-bundle.XXXXXX")"
_tarball="$(mktemp "$INSTALL_DIR/static/.taos-bundle.XXXXXX.tgz")"
# Verify the download against the CI-published SHA256 before extracting,
# so a corrupted or tampered tarball is rejected (we fall back to a local
# build). sha256sum on Linux, shasum -a 256 on macOS.
_exp_sha="$(curl -fsSL --max-time 20 "$_bundle_base/desktop-bundle.sha256" 2>/dev/null | tr -d '[:space:]')"
_sha_cmd="sha256sum"; command -v sha256sum >/dev/null 2>&1 || _sha_cmd="shasum -a 256"
if curl -fsSL --max-time 120 "$_bundle_base/desktop-bundle.tar.gz" -o "$_tarball" \
&& [[ -n "$_exp_sha" && "$($_sha_cmd "$_tarball" | awk '{print $1}')" == "$_exp_sha" ]] \
&& tar -C "$_stage" -xzf "$_tarball" \
&& [[ -f "$_stage/desktop/index.html" ]]; then
rm -rf "$INSTALL_DIR/static/desktop"
mkdir -p "$INSTALL_DIR/static"
mv /tmp/taos-bundle-stage/desktop "$INSTALL_DIR/static/desktop"
mv "$_stage/desktop" "$INSTALL_DIR/static/desktop"
# Match the repo owner so a later in-app rebuild (run as that user) can
# still write here; only meaningful when installing as root.
# still write here; only meaningful when installing as root. Trailing
# colon sets the owner's primary group (do not assume a group named
# after the user exists).
_own="$(stat -c '%U' "$INSTALL_DIR" 2>/dev/null || stat -f '%Su' "$INSTALL_DIR" 2>/dev/null || echo "")"
if [[ "$(id -u)" == "0" && -n "$_own" && "$_own" != "root" ]]; then
chown -R "$_own":"$_own" "$INSTALL_DIR/static/desktop" 2>/dev/null || true
chown -R "$_own:" "$INSTALL_DIR/static/desktop" 2>/dev/null || true
fi
_prebuilt_done=1
log "prebuilt desktop bundle installed into static/desktop/"
else
warn "prebuilt bundle download/extract failed; falling back to a local build"
fi
rm -rf /tmp/taos-bundle-stage /tmp/taos-desktop-bundle.tar.gz
rm -rf "$_stage" "$_tarball"
fi
fi

Expand Down
37 changes: 36 additions & 1 deletion tests/test_desktop_rebuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,14 @@ async def test_prebuilt_bundle_installed_on_tree_match(tmp_path, monkeypatch):
tar.addfile(info, io.BytesIO(payload))
tarball = buf.getvalue()

import hashlib

async def fake_to_thread(_fn, url, **_kwargs):
return "SHA123" if url.endswith("desktop-tree.txt") else tarball
if url.endswith("desktop-tree.txt"):
return "SHA123"
if url.endswith("desktop-bundle.sha256"):
return hashlib.sha256(tarball).hexdigest()
return tarball

monkeypatch.setattr(asyncio, "to_thread", fake_to_thread)

Expand Down Expand Up @@ -366,3 +372,32 @@ async def fake_exec(*args, **kwargs):

monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_exec)
assert await _try_prebuilt_desktop_bundle(tmp_path) is False


@pytest.mark.asyncio
async def test_prebuilt_bundle_rejected_on_checksum_mismatch(tmp_path, monkeypatch):
"""Tree matches but the published SHA256 does not -> build locally, no install."""
monkeypatch.setattr(asyncio, "create_subprocess_exec", _git_proc("SHA123"))

import io
import tarfile

buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
data = b"x"
info = tarfile.TarInfo("desktop/index.html")
info.size = len(data)
tar.addfile(info, io.BytesIO(data))
tarball = buf.getvalue()

async def fake_to_thread(_fn, url, **_kwargs):
if url.endswith("desktop-tree.txt"):
return "SHA123"
if url.endswith("desktop-bundle.sha256"):
return "deadbeef" * 8 # wrong digest
return tarball

monkeypatch.setattr(asyncio, "to_thread", fake_to_thread)

assert await _try_prebuilt_desktop_bundle(tmp_path) is False
assert not (tmp_path / "static" / "desktop").exists()
2 changes: 1 addition & 1 deletion tinyagentos/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.0-beta.4"
__version__ = "1.0.0-beta.4.1"
Loading
Loading