Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c190b44
docs(status): beta.6 shipped + branch cleanup + tooling + taOSgo kickoff
jaylfc Jun 21, 2026
8f046b6
feat(perf): auto-enable Reduce effects on weak GPUs + no-flash apply …
jaylfc Jun 21, 2026
ac2a934
fix(perf): do not trip the FPS probe when the tab is backgrounded (gi…
jaylfc Jun 21, 2026
8889871
feat(taosgo): proxy the cluster-join (consent-join) endpoints through…
jaylfc Jun 21, 2026
6524b90
Merge pull request #1289 from jaylfc/feat/perf-autodetect-reland
jaylfc Jun 21, 2026
73e7786
Merge pull request #1290 from jaylfc/feat/taosgo-cluster-join-proxy
jaylfc Jun 21, 2026
d6819e3
docs(status): perf-autodetect + cluster-join proxy merged; taOSgo con…
jaylfc Jun 21, 2026
69b7555
fix(install): make libtorrent an optional extra so fresh installs do …
jaylfc Jun 21, 2026
d143026
Merge pull request #1291 from jaylfc/fix/libtorrent-optional
jaylfc Jun 21, 2026
bfc85c1
docs(status): beta.7 install hotfix shipped (libtorrent optional)
jaylfc Jun 21, 2026
40d6608
fix(install): use a litellm-compatible Python (3.11-3.13) for the venv
jaylfc Jun 21, 2026
496b517
fix(install): provision Python via uv when no system 3.11-3.13 (gitar…
jaylfc Jun 21, 2026
c601224
Merge pull request #1293 from jaylfc/fix/python314-litellm
jaylfc Jun 21, 2026
563031d
docs(status): beta.8 install hotfix shipped + install audit + retrosp…
jaylfc Jun 21, 2026
c4b7302
fix(install): recreate a stale venv built with an unsupported Python
jaylfc Jun 21, 2026
e6f6002
docs(readme): note tested on WSL2 (Windows 11) default Ubuntu incl. P…
jaylfc Jun 21, 2026
3ff967d
fix(activity): hide the NPU card on hardware with no NPU
jaylfc Jun 21, 2026
0eeebbc
feat(desktop): default desktop widgets OFF until they are redesigned
jaylfc Jun 21, 2026
7955114
Merge pull request #1295 from jaylfc/fix/recreate-stale-venv
jaylfc Jun 21, 2026
95a0eb5
Merge pull request #1297 from jaylfc/fix/activity-npu-widgets-default
jaylfc Jun 21, 2026
1bf41b0
docs(status): beta.9 + live WSL validation + taOSgo security audit + …
jaylfc Jun 21, 2026
87ad0b6
providers: surface seeded models when a cloud probe returns none
jaylfc Jun 21, 2026
8fcce80
frontend: taOS agent model chooser lists the same models as the deplo…
jaylfc Jun 21, 2026
31f9e34
Merge remote-tracking branch 'origin/master' into chore/sync-dev-with…
jaylfc Jun 21, 2026
3a2bd5d
release: bump dev to 1.0.0-beta.10 (next cycle)
jaylfc Jun 21, 2026
f440409
Merge pull request #1300 from jaylfc/chore/sync-dev-with-master
jaylfc Jun 21, 2026
85f9a4f
Merge pull request #1299 from jaylfc/fix/deepseek-provider-models-picker
jaylfc Jun 21, 2026
a3a0c31
fix(deploy): gated master-key fallback when virtual keys unavailable …
jaylfc Jun 21, 2026
954bba8
fix(deploy): self-heal proxy-device-forbidden in restricted incus pro…
jaylfc Jun 21, 2026
f2bb4b0
fix(deploy): only master-key-fallback in routing-only mode, not on DB…
jaylfc Jun 21, 2026
426dcdf
Merge pull request #1301 from jaylfc/fix/agent-deploy-master-key-fall…
jaylfc Jun 21, 2026
6e80cf2
feat(cluster): manual (free-tier) worker pairing -- authorize {ip,cod…
jaylfc Jun 21, 2026
babf04f
cluster(ui): Add worker modal for manual free-tier pairing
jaylfc Jun 21, 2026
269ce56
worker: free-tier manual pairing mode (no announce)
jaylfc Jun 21, 2026
ed470a9
cluster: fold review findings on manual pairing (bug + security + cle…
jaylfc Jun 21, 2026
54a0a21
docs: document the manual Add-worker (LAN, no discovery) pairing flow
jaylfc Jun 21, 2026
3d1f2fa
Merge pull request #1298 from jaylfc/feat/cluster-manual-pairing
jaylfc Jun 21, 2026
2e5a5ac
release: 1.0.0-beta.11
jaylfc Jun 21, 2026
65d6fa9
Merge pull request #1302 from jaylfc/release/beta-11
jaylfc Jun 21, 2026
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ Versions follow semver beta: `1.0.0-beta.N`, bumped on each dev->master promotio

## [Unreleased]

## [1.0.0-beta.11] - 2026-06-21

### Fixed
- Agent deploy: framework agents (Hermes, OpenClaw, etc.) failed to deploy on hosts that cannot mint per-agent LiteLLM virtual keys (ARM / Pi where prisma cannot start, and any install without Postgres). The deployer now falls back to the shared LiteLLM master key in genuine routing-only mode (single-user instances, with a loud warning; opt out via `TAOS_DISABLE_AGENT_MASTER_KEY_FALLBACK=1`). A DB-configured-but-broken mint still fails loudly so a real fault is never masked.
- Agent deploy: containers in a restricted multi-user incus project (e.g. `user-999`) failed at creation because proxy devices were forbidden. `add_proxy_device` now self-heals by allowing proxy devices on the named project and retrying once.
- Provider model picker: a newly-added cloud provider (e.g. DeepSeek) whose `/models` probe needs a key now surfaces its seeded models, and the taOS agent model chooser lists the same models as the agent deploy picker.
- Activity NPU card hidden on hardware with no NPU; desktop widgets default off until redesigned.

### Added
- Cluster: free-tier manual worker pairing. A worker prints its LAN address and a PIN; the user adds it from Cluster > Add worker with no network discovery (taOSgo remains the automated path).

### Changed
- Hermes is the recommended default agent framework (shown first and pre-selected in the deploy wizard); OpenClaw second.
- Dev/master version reconciled (beta.6 drift fixed) and bumped to `1.0.0-beta.11`.

## [1.0.0-beta.9] - 2026-06-21

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ iwr -useb https://raw.githubusercontent.com/jaylfc/taOS/master/scripts/install-w

**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.

**Adding a worker manually (LAN, no discovery).** If you prefer not to broadcast on the network, run the installer with `TAOS_PAIR_MANUAL=1`. The worker prints its own LAN address and a one-time PIN instead of announcing itself. In taOS open Cluster, click **Add worker**, and enter that address and PIN: the controller authorises the pair and the worker's next poll claims its signing key. Nothing is advertised on the network; you type the two values by hand. The authorisation is single-use and expires after 15 minutes. (taOSgo, the optional remote-access tier, automates this end to end over the relay so there is no IP or PIN to copy.)

**Hardware detection on minimal systems.** The worker detects NVIDIA GPUs even when `nvidia-smi` is not installed: it probes `/proc/driver/nvidia` to confirm the driver is loaded and looks up VRAM from a known-cards table keyed by device ID. On native (non-container) hosts the installer offers to install `nvidia-utils` (via `apt`/`dnf`/`pacman`, matching the loaded driver branch automatically). Rockchip NPU detection uses unprivileged sysfs paths so it works inside LXC containers and other restricted environments where `/sys/kernel/debug` is inaccessible. Hosts that have neither GPU nor NPU are registered as CPU workers and contribute embeddings and small-model inference.

**Sudo and freshness.** The Linux/macOS worker installer is designed to run on **either a fresh Debian install or your existing system.** No clean slate required. It installs cleanly on Debian, Ubuntu, Fedora, Arch, Alpine, and macOS.
Expand Down
13 changes: 13 additions & 0 deletions desktop/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@
width: 100vw;
}
</style>

<!-- Reduce-effects: apply the saved choice before first paint so a low-end
device never flashes the full effects on load (#58). First-run GPU
auto-detect happens in-app via a frame-rate probe. -->
<script>
(function () {
try {
if (localStorage.getItem("taos-reduce-effects") === "on") {
document.documentElement.setAttribute("data-perf", "reduced");
}
} catch (e) {}
})();
</script>
</head>
<body>
<div id="root"></div>
Expand Down
4 changes: 2 additions & 2 deletions desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tinyagentos-desktop",
"private": true,
"version": "1.0.0-beta.9",
"version": "1.0.0-beta.11",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
6 changes: 6 additions & 0 deletions desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { NotificationToasts } from "@/components/NotificationToast";
import { NotificationCentre } from "@/components/NotificationCentre";
import { useNotificationStore } from "@/stores/notification-store";
import { useServerNotifications } from "@/hooks/use-server-notifications";
import { usePerfAutoDetect } from "@/lib/use-perf-autodetect";
import { TaosAssistantPanel } from "@/components/TaosAssistantPanel";
import { useTaosAgentStore } from "@/stores/taos-agent-store";
import { InstallPromptBanner } from "@/shell/InstallPromptBanner";
Expand Down Expand Up @@ -191,6 +192,11 @@ export function App() {

useSessionPersistence();

// First-run GPU auto-detect: probe the frame rate once and enable Reduce
// effects on a struggling device, so low-end hardware is smooth out of the
// box (#58). An explicit user choice is always honored and never overridden.
usePerfAutoDetect();

// Sync the persistent backend notification feed into the bell (desktop and
// mobile both render NotificationCentre under this component).
useServerNotifications();
Expand Down
6 changes: 4 additions & 2 deletions desktop/src/apps/ActivityApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,10 @@ export function ActivityApp({ windowId: _windowId }: { windowId: string }) {
</CardContent>
</Card>

{/* NPU */}
{(npu.cores || npu.type) && (
{/* NPU -- only when one is actually present. type "none" (or absent)
means no NPU, so the whole card is hidden rather than showing a
debugfs-permission message on hardware that has no NPU at all. */}
{npu.type && npu.type !== "none" && (
<Card className="col-span-12 md:col-span-6 p-4">
<CardContent className="p-0">
<div className="flex items-center justify-between mb-3">
Expand Down
123 changes: 120 additions & 3 deletions desktop/src/apps/ClusterApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MobileSplitView } from "@/components/mobile/MobileSplitView";
import {
Network, RefreshCw, ExternalLink, Copy, Check, Trash2, Wand2,
Cpu, MemoryStick, HardDrive, CircuitBoard, Zap, Server, Monitor,
X,
X, Plus,
} from "lucide-react";
import { Button, Card, CardContent } from "@/components/ui";
import type { ClusterWorker, WorkerStatus } from "@/lib/cluster";
Expand Down Expand Up @@ -634,6 +634,44 @@ export function ClusterApp({ windowId: _windowId }: { windowId: string }) {
setBusy(false);
}, [showToast]);

// Manual (free-tier) Add worker: the worker shows a PIN, the user enters its
// IP + PIN here, and the controller authorises that code so the worker's poll
// can claim its key. No discovery -- the automated path is taOSgo.
const [addOpen, setAddOpen] = useState(false);
const [addIp, setAddIp] = useState("");
const [addPin, setAddPin] = useState("");
const [addBusy, setAddBusy] = useState(false);

const submitAddWorker = useCallback(async () => {
const ip = addIp.trim();
const pin = addPin.trim();
if (!ip || !pin) {
showToast("Enter the worker IP and PIN");
return;
}
setAddBusy(true);
try {
const res = await fetch("/api/cluster/pairing/manual", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ url: ip, code: pin }),
});
if (res.ok) {
showToast("Worker authorised. It will connect shortly.");
setAddOpen(false);
setAddIp("");
setAddPin("");
fetchWorkers();
} else {
const j = await res.json().catch(() => ({}));
showToast(j.error ? `Add worker failed: ${j.error}` : `Add worker failed (${res.status})`);
}
} catch (e) {
showToast(e instanceof Error ? e.message : "Network error");
}
setAddBusy(false);
}, [addIp, addPin, showToast, fetchWorkers]);

return (
<div className="flex flex-col h-full min-h-0 overflow-hidden bg-shell-bg text-shell-text select-none">
{/* Toolbar */}
Expand All @@ -646,6 +684,16 @@ export function ClusterApp({ windowId: _windowId }: { windowId: string }) {
</span>
</div>
<div className="flex items-center gap-1.5 shrink-0">
<Button
variant="ghost"
size="sm"
onClick={() => setAddOpen(true)}
aria-label="Add a worker"
className="gap-1.5"
>
<Plus size={14} />
Add worker
</Button>
<label htmlFor="cluster-sort" className="sr-only">
Sort by
</label>
Expand Down Expand Up @@ -688,14 +736,24 @@ export function ClusterApp({ windowId: _windowId }: { windowId: string }) {
) : sortedWorkers.length === 0 ? (
<div className="flex flex-col items-center gap-2 py-6 text-center">
<p className="text-[11px] text-shell-text-tertiary">No workers registered yet.</p>
<Button
variant="secondary"
size="sm"
onClick={() => setAddOpen(true)}
className="gap-1.5"
aria-label="Add a worker"
>
<Plus size={14} />
Add worker
</Button>
<a
href="https://github.com/jaylfc/tinyagentos#distributed-compute-cluster"
target="_blank"
rel="noopener noreferrer"
className="text-[11px] px-3 py-1.5 rounded-md bg-white/5 border border-white/10 text-shell-text-secondary hover:bg-white/10 transition-colors"
className="text-[10px] text-shell-text-tertiary hover:text-shell-text-secondary underline underline-offset-2"
aria-label="How to add a worker (opens docs in new tab)"
>
How to add a worker
or read the docs
</a>
</div>
) : (
Expand Down Expand Up @@ -728,6 +786,65 @@ export function ClusterApp({ windowId: _windowId }: { windowId: string }) {
/>
</div>

{/* Add worker (manual / free-tier) modal */}
{addOpen && (
<div
className="absolute inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
role="dialog"
aria-modal="true"
aria-label="Add a worker"
onClick={() => !addBusy && setAddOpen(false)}
>
<div
className="w-full max-w-sm rounded-xl border border-white/10 bg-shell-bg-deep p-5 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-shell-text">Add a worker</h3>
<Button variant="ghost" size="icon" onClick={() => setAddOpen(false)} aria-label="Close">
<X size={14} />
</Button>
</div>
<p className="text-[11px] text-shell-text-tertiary mb-3 leading-relaxed">
Install the worker on the other machine. It shows a PIN. Enter that machine's
IP and the PIN below, then it joins the cluster. For one-tap setup from anywhere,
taOSgo handles this automatically.
</p>
<label className="block text-[10px] uppercase tracking-wide text-shell-text-tertiary mb-1" htmlFor="add-worker-ip">
Worker IP address
</label>
<input
id="add-worker-ip"
value={addIp}
onChange={(e) => setAddIp(e.target.value)}
placeholder="192.168.1.50"
autoComplete="off"
className="w-full h-9 mb-3 rounded-md border border-white/10 bg-shell-bg px-2.5 text-sm text-shell-text focus-visible:outline-none focus-visible:border-accent/40 focus-visible:ring-2 focus-visible:ring-accent/20"
/>
<label className="block text-[10px] uppercase tracking-wide text-shell-text-tertiary mb-1" htmlFor="add-worker-pin">
Pairing PIN
</label>
<input
id="add-worker-pin"
value={addPin}
onChange={(e) => setAddPin(e.target.value)}
placeholder="shown on the worker"
autoComplete="off"
onKeyDown={(e) => { if (e.key === "Enter") submitAddWorker(); }}
className="w-full h-9 mb-4 rounded-md border border-white/10 bg-shell-bg px-2.5 text-sm font-mono tracking-widest text-shell-text focus-visible:outline-none focus-visible:border-accent/40 focus-visible:ring-2 focus-visible:ring-accent/20"
/>
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={() => setAddOpen(false)} disabled={addBusy}>
Cancel
</Button>
<Button variant="default" size="sm" onClick={submitAddWorker} disabled={addBusy}>
{addBusy ? "Authorising..." : "Add worker"}
</Button>
</div>
</div>
</div>
)}

{/* Toast */}
{toast && (
<div
Expand Down
19 changes: 13 additions & 6 deletions desktop/src/apps/agents/DeployWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -454,13 +454,20 @@ export function DeployWizard({
description: String(a.description ?? ""),
verification_status: (a.verification_status as Framework["verification_status"]) ?? "alpha",
}));
// openclaw first, then preserve API order
mapped.sort((a, b) => {
if (a.id === "openclaw") return -1;
if (b.id === "openclaw") return 1;
return 0;
});
// Hermes is the recommended default and shows first; OpenClaw
// second; the rest preserve API order.
const rank = (id: string) => (id === "hermes" ? 0 : id === "openclaw" ? 1 : 2);
mapped.sort((a, b) => rank(a.id) - rank(b.id));
setFrameworks(mapped);
// Default-select Hermes (or the first visible framework) so the
// wizard opens on a working choice instead of nothing selected.
setSelectedFramework((cur) => {
if (cur) return cur;
const preferred = mapped.find((f) => f.id === "hermes")
?? mapped.find((f) => f.verification_status !== "alpha")
?? mapped[0];
return preferred?.id ?? "";
});
}
}
} catch { /* leave frameworks empty, wizard will show nothing selectable */ }
Expand Down
13 changes: 7 additions & 6 deletions desktop/src/components/TaosAgentCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { ModelPickerModal } from "@/components/ModelPickerModal";
import type { AgentModel } from "@/components/ModelPickerFlow";
import { loadAgentModels } from "@/lib/models";
import {
fetchTaosAgentConfig,
setTaosAgentModel,
Expand Down Expand Up @@ -51,12 +52,12 @@ export function TaosAgentCard() {
async function ensureModelsLoaded() {
if (modelsLoaded) return;
try {
const res = await fetch("/api/providers/models?refresh=true", {
headers: { Accept: "application/json" },
});
const data = res.ok ? await res.json() : { data: [] };
setModels((data.data ?? []).map((m: { id: string }) => ({
id: m.id, name: m.id, hostKind: "cloud" as const,
// Use the same unified loader as the agent deploy picker so the taOS
// agent chooser lists exactly the same models (controller-catalog,
// cluster-worker, and cloud-provider models keyed back to a provider).
const aggregated = await loadAgentModels();
setModels(aggregated.map((m) => ({
id: m.id, name: m.name, host: m.host, hostKind: m.hostKind,
})));
} catch { /* leave empty */ }
finally { setModelsLoaded(true); }
Expand Down
32 changes: 7 additions & 25 deletions desktop/src/components/TaosAssistantSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,10 @@ import { TaosAssistantSettings } from "./TaosAssistantSettings";
import { useTaosAgentStore } from "@/stores/taos-agent-store";

vi.mock("@/lib/models", () => ({
fetchClusterWorkers: vi.fn(),
fetchCloudProviders: vi.fn(),
workersToAggregated: vi.fn(),
cloudProvidersToAggregated: vi.fn(),
localProvidersToAggregated: vi.fn(),
loadAgentModels: vi.fn(),
}));

import {
fetchClusterWorkers,
fetchCloudProviders,
workersToAggregated,
cloudProvidersToAggregated,
localProvidersToAggregated,
} from "@/lib/models";
import { loadAgentModels } from "@/lib/models";

const mockFetch = vi.fn();

Expand Down Expand Up @@ -52,11 +42,7 @@ describe("TaosAssistantSettings", () => {
beforeEach(() => {
resetStore();
setupFetch();
vi.mocked(fetchClusterWorkers).mockResolvedValue([]);
vi.mocked(fetchCloudProviders).mockResolvedValue([]);
vi.mocked(workersToAggregated).mockReturnValue([]);
vi.mocked(cloudProvidersToAggregated).mockReturnValue([]);
vi.mocked(localProvidersToAggregated).mockReturnValue([]);
vi.mocked(loadAgentModels).mockResolvedValue([]);
});

afterEach(() => {
Expand Down Expand Up @@ -107,24 +93,20 @@ describe("TaosAssistantSettings", () => {
expect(onClose).toHaveBeenCalledTimes(1);
});

it("fetches models on open and passes loaded state to ModelPickerFlow", async () => {
vi.mocked(fetchClusterWorkers).mockResolvedValue([]);
vi.mocked(fetchCloudProviders).mockResolvedValue([]);
it("loads the unified agent model list on open", async () => {
render(<TaosAssistantSettings open={true} onClose={vi.fn()} />);

await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith("/api/models");
expect(loadAgentModels).toHaveBeenCalledTimes(1);
});
expect(fetchClusterWorkers).toHaveBeenCalledTimes(1);
expect(fetchCloudProviders).toHaveBeenCalledTimes(1);
});

it("sends PATCH to /api/taos-agent/settings with selected model and closes", async () => {
const onClose = vi.fn();
useTaosAgentStore.setState({ model: "old-model" });

vi.mocked(localProvidersToAggregated).mockReturnValue([
{ id: "llama-3", name: "Llama 3", host: "controller", hostKind: "controller" },
vi.mocked(loadAgentModels).mockResolvedValue([
{ key: "controller:llama-3", id: "llama-3", name: "Llama 3", host: "controller", hostKind: "controller" },
]);

render(<TaosAssistantSettings open={true} onClose={onClose} />);
Expand Down
Loading
Loading