Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4b39d45
fix(rknpu): a live rkllama port is not 'installed' unless it is a man…
jaylfc Jul 9, 2026
d8342c3
docs(design): cluster-aware backend service management spec (#1757)
jaylfc Jul 9, 2026
d8c555a
feat(catalog): managed-backend-service manifest contract + CI lint (P…
jaylfc Jul 9, 2026
8e901b4
feat(cluster): node-local backend service manager core (Phase 1.2) (#…
jaylfc Jul 9, 2026
8bb607b
refactor(system): derive #1743 restart targets from managed-backend c…
jaylfc Jul 9, 2026
71939f1
feat(models): atomic VRAM check-and-reserve before model load (TOCTOU…
hognek Jul 9, 2026
6b485fb
feat(cluster): advertise available models from Skald sidecar manifest…
hognek Jul 9, 2026
9ac0ff7
fix(install-rknpu): pin rkllama to the 1.3.0 ref + guard the fork pat…
jaylfc Jul 9, 2026
58cd159
fix(worker,models): audit hotfixes - manifest crash, VRAM fail-open, …
jaylfc Jul 9, 2026
f221bff
feat(a2a): authenticated bus send proxy (agents post as themselves, n…
jaylfc Jul 10, 2026
bd86020
test(designstudio): add coverage for the untested Design Studio app (…
jaylfc Jul 10, 2026
55b6ef3
feat(taosgo): persist per-host mesh service credentials at cluster-jo…
jaylfc Jul 10, 2026
7c6f3ba
fix(catalog): fix 6 broken install-script references in agent manifes…
hognek Jul 10, 2026
02ef089
feat(taosgo): headless Headscale mesh-join via system tailscale (Slic…
jaylfc Jul 10, 2026
307c6b8
feat(gamestudio): AI texture/sprite generation via ComfyUI backend (S…
jaylfc Jul 10, 2026
da9a896
feat(agents): least-privilege project_tasks registry scope for kanban…
jaylfc Jul 10, 2026
b8acce7
release: 1.0.0-beta.39 (Game Studio textures, taOSgo mesh-join, proje…
jaylfc Jul 10, 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
5 changes: 5 additions & 0 deletions .github/workflows/doc-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ jobs:
env:
BASE_REF: ${{ github.base_ref }}
run: python scripts/check_doc_gate.py diff-gate --base "origin/$BASE_REF"

- name: Managed backend manifest lint
run: |
python -m pip install --quiet pyyaml
python scripts/check_manifests.py managed-lint
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ data/.setup_complete
data/.auth_password
data/.auth_sessions
data/.auth_local_token
data/mesh_credentials.json
data/agents.json
data/images/
data/videos/
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ Versions follow semver beta: `1.0.0-beta.N`, bumped on each dev->master promotio

## [Unreleased]

## [1.0.0-beta.39] - 2026-07-10

### Added
- Game Studio can now generate textures and sprites from a text prompt using a ComfyUI backend on a discrete-GPU worker, writing the image straight into the game's file set. On a host with no capable GPU the panel shows a clear "needs a GPU worker" state instead of failing (#1773).
- taOSgo cluster-join now completes the network side: a controller joins the account mesh over the system tailscale against the Headscale server, and the per-host service tokens the join returns are persisted host-locally (owner-only) so publishing and passkey fetches keep working after a join (#1770, #1772).
- Agents post to the coordination bus as themselves through an authenticated send proxy, so a message carries the agent's own identity and cannot be spoofed as another account (#1768).
- The cluster advertises the models a node can serve from its backend manifest, and installing a backend now registers it as a managed, node-local service that can be started, stopped, and health-checked per node (#1756, #1758, #1760, #1762).
- An approved external agent can be granted a least-privilege project-tasks scope to read and drive a single project's task board (claim, close, comment) with its own token, scoped so it can never reach another project (#1774).

### Fixed
- Backend and worker robustness: the model VRAM check reserves atomically before a load so two loads cannot race the same memory, a malformed backend manifest no longer crashes the worker, and the VRAM guard fails closed rather than open on a probe error (#1725, #1767).
- The RK3588 (RKLLM) install path pins the rkllama server to the verified 1.3.0 reference and guards the fork patches, and a live rkllama port is treated as installed only when it is a managed service (#1755, #1764).
- Fixed six agent-framework catalog manifests that referenced install scripts which did not exist at the repo root (#1694).

## [1.0.0-beta.38] - 2026-07-08

### Added
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,20 @@ curl -fsSL https://raw.githubusercontent.com/jaylfc/taOS/master/scripts/install-

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.

## Agent Framework Install Scripts

Several agent frameworks ship with dedicated install scripts under `scripts/install-*.sh` that are invoked automatically by the catalog installer when deploying the corresponding framework:

| Script | Framework | Build toolchain |
|---|---|---|
| `scripts/install-agent-zero.sh` | [Agent Zero](https://github.com/frdel/agent-zero) | Clones repo to `/opt/agent-zero` and installs with pip |
| `scripts/install-deer-flow.sh` | [DeerFlow](https://github.com/bytedance/deer-flow) | Clones repo to `/opt/deer-flow` and provisions with `uv` (Python 3.12) |
| `scripts/install-moltis.sh` | [Moltis](https://github.com/moltis-org/moltis) | Installs via `cargo install` from crates.io (or git tag) |
| `scripts/install-openclaw.sh` | [OpenClaw](https://github.com/openclaw/openclaw) | Clones repo to `/opt/openclaw` and installs with pip |
| `scripts/install-picoclaw.sh` | [PicoClaw](https://github.com/sipeed/picoclaw) | Clones repo to `/opt/picoclaw` and builds with `cmake` |

Hermes Agent uses `install: {method: pip, package: hermes-agent}` in its catalog manifest (available on PyPI from Nous Research) and does not need a separate install script.

## TurboQuant KV cache compression

**768K context window on a single RTX 3060 (12 GB).** taOS integrates Google's TurboQuant (ICLR 2026) KV cache quantization via TheTom/llama-cpp-turboquant. Unlike weight quantization, which compresses model files, TurboQuant compresses the per-request KV cache -- the per-token memory that scales with context length and is the actual bottleneck on consumer hardware.
Expand Down
4 changes: 4 additions & 0 deletions app-catalog/agents/agent-zero/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ requires:
install:
method: script
script: scripts/install-agent-zero.sh
note: |
Clones frdel/agent-zero into /opt/agent-zero and installs with pip.
No PyPI package — the `agent-zero` package on PyPI is an unrelated
voice-agent framework. Agent Zero is clone-and-run only.

hardware_tiers:
arm-npu-16gb: full
Expand Down
7 changes: 3 additions & 4 deletions app-catalog/agents/deer-flow/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ requires:

install:
method: script
script: scripts/install.sh
script: scripts/install-deer-flow.sh
note: |
Clones bytedance/deer-flow into /opt/deer-flow and provisions the backend
with uv (Python 3.12). Writes a config.yaml pointing DeerFlow at the taOS
LiteLLM proxy and a systemd unit that serves the backend runs API on 8001.
The bridge adapter (deer_flow_adapter.py) lives in the taOS controller.
with uv (Python 3.12). The bridge adapter (deer_flow_adapter.py) lives in
the taOS controller.

config_schema:
- name: model
Expand Down
8 changes: 2 additions & 6 deletions app-catalog/agents/hermes/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@ requires:
python: ">=3.10"

install:
method: script
script: scripts/install.sh
note: |
Installs hermes-agent from PyPI inside the agent container.
Configures the Hermes → TAOS bridge adapter (hermes_adapter.py)
and a systemd unit that connects to TAOS channels via SSE.
method: pip
package: hermes-agent

config_schema:
- name: model
Expand Down
6 changes: 5 additions & 1 deletion app-catalog/agents/openclaw/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ requires:

install:
method: script
script: scripts/install.sh
script: scripts/install-openclaw.sh
note: |
Clones openclaw/openclaw into /opt/openclaw and installs with pip.
No PyPI package — `openclaw` on PyPI resolves to an unrelated CMDOP
plugin. OpenClaw is clone-and-run only.

config_schema:
- name: model
Expand Down
7 changes: 7 additions & 0 deletions app-catalog/services/rkllama/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ lifecycle:
backend_type: rkllama
default_url: http://localhost:7833
auto_manage: true
# Managed as a systemd service (see scripts/install-rknpu.sh install_systemd_unit).
# taOS manages the unit per node: start/stop/restart default to `systemctl <verb> <unit>`.
unit: rkllama.service
scope: system
health:
url: "http://localhost:7833/api/tags"
expect: '"models"'
keep_alive_minutes: 0
start_cmd: "systemctl start rkllama"
stop_cmd: "systemctl stop rkllama"
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.38",
"version": "1.0.0-beta.39",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
137 changes: 137 additions & 0 deletions desktop/src/apps/designstudio/DesignView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
import { useState } from "react";
import { render, screen, fireEvent } from "@testing-library/react";

/* react-konva needs a real <canvas> (the `canvas` npm package), which is not
* installed in the jsdom test env. Mock the primitives to inert passthroughs so
* DesignView's toolbar / layers / properties logic can be exercised without a
* Konva stage. Because the Stage/Transformer refs are never populated, the
* canvas-only effects (transformer wiring, PNG export) safely no-op. */
vi.mock("react-konva", () => ({
Stage: ({ children }: { children?: React.ReactNode }) => <div data-konva="stage">{children}</div>,
Layer: ({ children }: { children?: React.ReactNode }) => <div data-konva="layer">{children}</div>,
Rect: () => null,
Text: () => null,
Image: () => null,
Ellipse: () => null,
Line: () => null,
Transformer: () => null,
}));

import { DesignView } from "./DesignView";
import { DEFAULT_ARTBOARD, type CanvasElement } from "./types";

/** DesignView is a controlled component: it hands the next elements array back
* through onElementsChange and re-reads it from props. This host mirrors how
* DesignStudioApp owns that state, and exposes the latest array for assertions. */
let latest: CanvasElement[] = [];
function Host() {
const [els, setEls] = useState<CanvasElement[]>([]);
latest = els;
return <DesignView elements={els} onElementsChange={setEls} artboard={DEFAULT_ARTBOARD} />;
}

function renderView() {
return render(<Host />);
}

beforeAll(() => {
vi.stubGlobal(
"ResizeObserver",
class {
observe() {}
unobserve() {}
disconnect() {}
},
);
});

afterEach(() => {
latest = [];
vi.clearAllMocks();
});

describe("DesignView", () => {
it("renders the empty state with export/undo/redo disabled", () => {
renderView();
expect(screen.getByText(/Add an element or generate with Magic/i)).toBeDefined();
expect((screen.getByLabelText("Export as PNG") as HTMLButtonElement).disabled).toBe(true);
expect((screen.getByLabelText("Undo") as HTMLButtonElement).disabled).toBe(true);
expect((screen.getByLabelText("Redo") as HTMLButtonElement).disabled).toBe(true);
expect(screen.getByText(/No layers yet/i)).toBeDefined();
});

it("keeps the coming-soon Star tile disabled", () => {
renderView();
expect((screen.getByLabelText("Star") as HTMLButtonElement).disabled).toBe(true);
expect((screen.getByLabelText("Text") as HTMLButtonElement).disabled).toBe(false);
});

it("adding a text element updates state and adds a layer, enabling export/undo", () => {
renderView();
fireEvent.click(screen.getByLabelText("Text"));

expect(latest).toHaveLength(1);
expect(latest[0].type).toBe("text");
expect(screen.getByLabelText(/Select layer/i)).toBeDefined();
expect((screen.getByLabelText("Export as PNG") as HTMLButtonElement).disabled).toBe(false);
expect((screen.getByLabelText("Undo") as HTMLButtonElement).disabled).toBe(false);
});

it("adding a shape creates a rect and auto-selects it (properties panel appears)", () => {
renderView();
fireEvent.click(screen.getByLabelText("Shape"));

expect(latest[0].type).toBe("rect");
// Auto-selection surfaces the shape's fill/stroke controls.
expect(screen.getByLabelText("Fill color")).toBeDefined();
expect(screen.getByText("Rectangle")).toBeDefined();
});

it("adding a circle creates an ellipse", () => {
renderView();
fireEvent.click(screen.getByLabelText("Circle"));
expect(latest[0].type).toBe("ellipse");
});

it("adding a line creates a line element", () => {
renderView();
fireEvent.click(screen.getByLabelText("Line"));
expect(latest[0].type).toBe("line");
});

it("undo removes a just-added element and returns to the empty state", () => {
renderView();
fireEvent.click(screen.getByLabelText("Text"));
expect(latest).toHaveLength(1);
fireEvent.click(screen.getByLabelText("Undo"));
expect(latest).toHaveLength(0);
expect(screen.getByText(/Add an element or generate with Magic/i)).toBeDefined();
});

it("brand swatches are disabled until a fillable element is selected", () => {
renderView();
expect((screen.getByLabelText("Apply #6b7689") as HTMLButtonElement).disabled).toBe(true);

fireEvent.click(screen.getByLabelText("Shape"));
expect((screen.getByLabelText("Apply #6b7689") as HTMLButtonElement).disabled).toBe(false);
});

it("exposes zoom controls that fit the artboard to the stage", () => {
renderView();
const zoom = screen.getByLabelText("Zoom level") as HTMLSelectElement;
// The view fits the artboard on mount, so the zoom is a positive percentage.
expect(Number(zoom.value)).toBeGreaterThan(0);
expect(screen.getByLabelText("Zoom in")).toBeDefined();
expect(screen.getByLabelText("Zoom out")).toBeDefined();
expect(screen.getByLabelText("Fit to screen")).toBeDefined();
});

it("shows the artboard name and dimensions in the header", () => {
renderView();
expect(screen.getByText(DEFAULT_ARTBOARD.name)).toBeDefined();
expect(
screen.getByText(`${DEFAULT_ARTBOARD.width} x ${DEFAULT_ARTBOARD.height}`),
).toBeDefined();
});
});
105 changes: 105 additions & 0 deletions desktop/src/apps/designstudio/LayersPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { LayersPanel } from "./LayersPanel";
import type { CanvasElement } from "./types";

function text(id: string, zIndex: number, over: Partial<CanvasElement> = {}): CanvasElement {
return {
id,
type: "text",
x: 0,
y: 0,
width: 100,
height: 40,
rotation: 0,
zIndex,
visible: true,
text: "Hello world",
fontSize: 32,
fontFamily: "Inter",
fill: "#fff",
align: "left",
...over,
} as CanvasElement;
}

function baseProps() {
return {
onSelect: vi.fn(),
onToggleVisibility: vi.fn(),
onReorder: vi.fn(),
onDelete: vi.fn(),
};
}

describe("LayersPanel", () => {
it("shows an empty state with no layers", () => {
render(<LayersPanel elements={[]} selectedId={null} {...baseProps()} />);
expect(screen.getByText(/No layers yet/i)).toBeDefined();
});

it("lists layers top-most (highest zIndex) first", () => {
const els = [text("low", 0, { text: "Bottom" }), text("high", 5, { text: "Top" })];
render(<LayersPanel elements={els} selectedId={null} {...baseProps()} />);
const labels = screen.getAllByText(/Top|Bottom/).map((n) => n.textContent);
expect(labels[0]).toBe("Top");
expect(labels[1]).toBe("Bottom");
});

it("labels typed layers by their content", () => {
const els = [
text("t", 3),
{ ...text("r", 2), type: "rect", fill: "#000", stroke: "#fff", strokeWidth: 0 } as CanvasElement,
{ ...text("l", 1), type: "line", stroke: "#fff", strokeWidth: 3 } as CanvasElement,
];
render(<LayersPanel elements={els} selectedId={null} {...baseProps()} />);
expect(screen.getByText("Hello world")).toBeDefined();
expect(screen.getByText("Rectangle")).toBeDefined();
expect(screen.getByText("Line")).toBeDefined();
});

it("fires onSelect with the clicked layer id", () => {
const props = baseProps();
render(<LayersPanel elements={[text("a", 0)]} selectedId={null} {...props} />);
fireEvent.click(screen.getByLabelText(/Select layer/i));
expect(props.onSelect).toHaveBeenCalledWith("a");
});

it("marks the selected layer via aria-pressed", () => {
render(<LayersPanel elements={[text("a", 0)]} selectedId="a" {...baseProps()} />);
expect(screen.getByLabelText(/Select layer/i).getAttribute("aria-pressed")).toBe("true");
});

it("disables move-up on the top layer and move-down on the bottom layer", () => {
const els = [text("bottom", 0, { text: "Bottom" }), text("top", 5, { text: "Top" })];
render(<LayersPanel elements={els} selectedId={null} {...baseProps()} />);
// Top row (rendered first) can't move up; bottom row can't move down.
expect((screen.getByLabelText("Move Top up") as HTMLButtonElement).disabled).toBe(true);
expect((screen.getByLabelText("Move Bottom down") as HTMLButtonElement).disabled).toBe(true);
expect((screen.getByLabelText("Move Top down") as HTMLButtonElement).disabled).toBe(false);
});

it("fires onReorder with the direction", () => {
const props = baseProps();
const els = [text("bottom", 0, { text: "Bottom" }), text("top", 5, { text: "Top" })];
render(<LayersPanel elements={els} selectedId={null} {...props} />);
fireEvent.click(screen.getByLabelText("Move Top down"));
expect(props.onReorder).toHaveBeenCalledWith("top", "down");
});

it("toggles visibility with the correct label per state", () => {
const props = baseProps();
const els = [text("a", 0), text("b", 1, { visible: false, text: "Hidden" })];
render(<LayersPanel elements={els} selectedId={null} {...props} />);
fireEvent.click(screen.getByLabelText("Hide Hello world"));
expect(props.onToggleVisibility).toHaveBeenCalledWith("a");
expect(screen.getByLabelText("Show Hidden")).toBeDefined();
});

it("fires onDelete for the layer", () => {
const props = baseProps();
render(<LayersPanel elements={[text("a", 0)]} selectedId={null} {...props} />);
fireEvent.click(screen.getByLabelText("Delete Hello world"));
expect(props.onDelete).toHaveBeenCalledWith("a");
});
});
Loading
Loading