Skip to content

fix(worker,models): audit hotfixes - manifest crash, VRAM fail-open, real min_ram_mb key#1767

Merged
jaylfc merged 1 commit into
devfrom
fix/audit-hotfix-vram-manifest
Jul 9, 2026
Merged

fix(worker,models): audit hotfixes - manifest crash, VRAM fail-open, real min_ram_mb key#1767
jaylfc merged 1 commit into
devfrom
fix/audit-hotfix-vram-manifest

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Hotfix for the urgent findings from the full code audit of the recently merged work. The deeper redesign parts stay with #1765 / #1766.

What

  1. Worker manifest can no longer brick the worker (HIGH, fix(worker): manifest path crashes worker on malformed input + availability depends on backend liveness (audit follow-up to #1762) #1765 item 1): malformed/non-object /etc/taos/worker-models.json degrades to no-manifest with a logged warning; enrichment skips entries without model_id and is wrapped fail-soft; heartbeat failures are logged before being swallowed.
  2. VRAM reservation fails open on no-probe hardware (HIGH, fix(models): VRAM reservation fails closed on non-NVIDIA + TOCTOU gate reads dead manifest key (audit follow-up to #1725) #1766 item 1): probe returns None (cannot know) vs (0,0) (known-full); reserve() admits bookkeeping-only on None, matching claim_lease semantics. Previously every pull with a VRAM requirement 503'd on AMD/Apple/Rockchip. Probe moved to asyncio.to_thread (no more event-loop stall under the lock). stats() gains probe_available.
  3. TOCTOU gate reads the real key (MEDIUM, fix(models): VRAM reservation fails closed on non-NVIDIA + TOCTOU gate reads dead manifest key (audit follow-up to #1725) #1766 item 2): the rkllama pull gate now takes the max of variant.requires.backends[].min_ram_mb (falling back to the variant-level key), so it is no longer dead code against the entire catalog.
  4. backend_services.load_managed_backends logs skipped manifests (silent-drop drift fix).
  5. install-rknpu.sh: stale "copy-of-truth" comment corrected (old port 8080 / old models path).

Tests

  • Updated the two tests that blessed the crash to assert the degrade behaviour.
  • New: non-object manifest degrade, no-probe fail-open (admission + bookkeeping + release), probe_available in stats, and a regression guard that a REAL probe showing insufficient VRAM still denies.
  • Local: 155 tests green across worker_manifest / vram_reservation / worker / cluster_worker_protocol / backend_services / routes_system / routes_models; compileall + bash -n clean.

Summary by CodeRabbit

  • New Features

    • VRAM checks now “fail open” when hardware probing is unavailable, allowing jobs to proceed while still tracking reservation state.
    • Model download sizing now uses the most demanding backend requirement, improving capacity checks.
  • Bug Fixes

    • Malformed or missing worker manifests now fall back to a safe empty state instead of failing.
    • Worker backend detection and heartbeat reporting are more resilient, with clearer warnings when input or local processing is invalid.
  • Chores

    • Updated deployment notes and improved warning logs for invalid backend definitions.

…real min_ram_mb key

Fixes the urgent findings from the full code audit of the recent merges
(follow-ups #1765/#1766 track the deeper redesign parts):

- worker_manifest.load_manifest: a malformed or non-object manifest file is
  external input and now degrades to the empty manifest with a logged
  warning instead of raising. Previously one typo'd comma in
  /etc/taos/worker-models.json crash-looped the whole worker under systemd.
- worker/agent.py enrichment: skip entries without model_id (logged) and
  wrap the whole enrichment in a fail-soft try/except so no manifest problem
  can take detect_backends down. Heartbeat send failures are now logged
  before being swallowed so a payload-build bug no longer masquerades as
  controller unreachability.
- vram_reservation: the probe now returns None (cannot know) distinct from
  (0, 0) (known-full), and reserve() fails OPEN with bookkeeping-only
  admission when no probe exists, matching cluster claim_lease semantics.
  Previously every reservation was denied 503 on AMD/Apple/Rockchip, i.e.
  on exactly the rkllama hardware the guard was written for. The probe also
  moved to asyncio.to_thread so nvidia-smi no longer blocks the event loop
  while holding the reservation lock. stats() gains probe_available.
- routes/models.py: the rkllama pull gate now reads the real backend-level
  requirement (max of variant.requires.backends[].min_ram_mb, falling back
  to the variant key). Every rkllm variant has variant-level min_ram_mb 0,
  so the previous read made the TOCTOU gate dead code against the entire
  catalog.
- cluster/backend_services.load_managed_backends: skipped manifests are now
  logged so a device-side catalog edit cannot silently drop a backend out
  of #1743 recovery.
- install-rknpu.sh: replace the stale copy-of-truth comment (old port 8080
  and pre-consolidation models path) with an accurate one.

Tests: the two tests that blessed the crash now assert the degrade
behaviour, plus new coverage for non-object manifests, no-probe fail-open,
probe-unavailable stats, and a regression guard that a real probe showing
insufficiency still denies.

Docs-Reviewed: behaviour hardening within existing features; no documented
install step, endpoint path, or response shape changed (installer diff is a
comment correction only).
@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 →

@jaylfc
jaylfc enabled auto-merge (squash) July 9, 2026 20:56
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Runtime resilience and resource handling

Layer / File(s) Summary
VRAM probe and reservation semantics
tinyagentos/vram_reservation.py, tinyagentos/routes/models.py, tests/test_vram_reservation.py
Unavailable VRAM probes now fail open for reservations, known capacity remains enforced, monitoring reports probe availability, and model estimates use backend requirements.
Manifest validation and worker diagnostics
tinyagentos/worker/worker_manifest.py, tinyagentos/worker/agent.py, tinyagentos/cluster/backend_services.py, tests/test_worker_manifest.py
Malformed manifests and invalid backend entries degrade safely, while manifest, backend-loading, enrichment, and heartbeat failures emit warnings.
Installer unit comment alignment
scripts/install-rknpu.sh
The rkllama systemd ExecStart comment now reflects port 7833, the unified models root, and shared model preloading.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkerAgent
  participant load_manifest
  participant ProbedBackends
  WorkerAgent->>load_manifest: load and validate manifest
  load_manifest-->>WorkerAgent: manifest or empty fallback
  WorkerAgent->>ProbedBackends: apply compatible model entries
  ProbedBackends-->>WorkerAgent: enriched backend data
Loading

Possibly related issues

  • jaylfc/taOS#1766: Covers the same VRAM fail-open behavior and backend-level VRAM estimation changes.
  • jaylfc/taOS#1765: Covers the malformed manifest crash path and heartbeat diagnostics.

Possibly related PRs

  • jaylfc/taOS#802: Previously changed the installer’s rkllama default port from 8080 to 7833.
  • jaylfc/taOS#1725: Added related atomic VRAM reservation logic and tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main hotfix areas: manifest handling, VRAM fail-open behavior, and the min_ram_mb routing fix.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-hotfix-vram-manifest

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.

@gitar-bot

gitar-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

# requires.backends[].min_ram_mb, so reading only the variant key made
# this gate dead code (audit follow-up, #1766). Take the max across
# required backends, falling back to the variant-level key.
_backend_reqs = (variant.get("requires") or {}).get("backends") or []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: variant.get("requires") or {} does not guard against requires being a non-dict truthy value

(variant.get("requires") or {}) only falls back to {} when the value is falsy. If requires is present but a list/string (e.g. a malformed catalog variant — exactly the kind of untrusted external input this PR's theme defends against), the truthy value is kept and .get("backends") raises AttributeError, 500-ing the download route. Guard with isinstance instead.

Suggested change
_backend_reqs = (variant.get("requires") or {}).get("backends") or []
_requires = variant.get("requires")
_backend_reqs = ((_requires if isinstance(_requires, dict) else {}) or {}).get("backends") or []

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

# Probe in a thread: nvidia-smi is a blocking subprocess and this
# runs on a request hot path while holding the lock.
probe = await asyncio.to_thread(self._probe_vram)
if probe is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Fail-open conflates "no nvidia-smi" with "probe errored", over-admitting on real NVIDIA hardware

probe is None now covers both "this host has no VRAM probe" (AMD/Apple/Rockchip) and "nvidia-smi failed transiently". Previously a failed probe returned (0, 0) and the reservation was denied (fail-closed). Now any transient nvidia-smi error on actual NVIDIA GPUs admits the reservation unenforced, so concurrent pulls can over-commit and OOM — the exact failure mode this guard was originally added to prevent (#1706).

Consider letting _probe_vram distinguish "binary absent" (fail open, bookkeeping only) from "probe errored" (fail closed or retry) rather than collapsing both into None.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/routes/models.py 395 variant.get("requires") or {} does not guard non-dict truthy requires; malformed variant raises AttributeError and 500s the download route
tinyagentos/vram_reservation.py 93 Fail-open conflates "no probe" with "transient probe error"; transient nvidia-smi failure on real NVIDIA GPUs now over-admits and can OOM
Files Reviewed (8 files)
  • scripts/install-rknpu.sh - comment only, 0 issues
  • tests/test_vram_reservation.py - 0 issues
  • tests/test_worker_manifest.py - 0 issues
  • tinyagentos/cluster/backend_services.py - 0 issues
  • tinyagentos/routes/models.py - 1 issue
  • tinyagentos/vram_reservation.py - 1 issue
  • tinyagentos/worker/agent.py - 0 issues
  • tinyagentos/worker/worker_manifest.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by hy3-20260706:free · Input: 73.6K · Output: 8.5K · Cached: 154K

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tinyagentos/vram_reservation.py (1)

165-191: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

available_vram() and stats() still probe synchronously, blocking the event loop

reserve() correctly moved the probe to asyncio.to_thread, but available_vram() and stats() still call _probe_vram() directly. In tinyagentos/routes/models.py line 405, available_vram() is called from async def download_model on the denial path, causing a 50–200 ms blocking nvidia-smi call that stalls all concurrent coroutines. This is also a redundant probe — reserve() already probed via asyncio.to_thread but discards the result on denial.

Consider making available_vram() async (or caching the last probe result from reserve()) so the denial path doesn't block. Verify that stats() callers aren't async either.

♻️ Suggested fix: make available_vram() async
-    def available_vram(self) -> tuple[int, int]:
-        """Return ``(effective_free_mb, total_mb)`` after subtracting
-        in-flight reservations from the hardware probe.
-
-        When no probe is available, returns ``(0, 0)``; callers on that
-        path should already have been admitted fail-open by :meth:`reserve`
-        (a deny message never reports these zeros as real capacity).
-        """
-        probe = self._probe_vram()
-        if probe is None:
-            return 0, 0
-        free_mb, total_mb = probe
-        effective_free = max(0, free_mb - self._reserved_vram_mb)
-        return effective_free, total_mb
+    async def available_vram(self) -> tuple[int, int]:
+        """Return ``(effective_free_mb, total_mb)`` after subtracting
+        in-flight reservations from the hardware probe.
+
+        When no probe is available, returns ``(0, 0)``; callers on that
+        path should already have been admitted fail-open by :meth:`reserve`
+        (a deny message never reports these zeros as real capacity).
+        """
+        probe = await asyncio.to_thread(self._probe_vram)
+        if probe is None:
+            return 0, 0
+        free_mb, total_mb = probe
+        effective_free = max(0, free_mb - self._reserved_vram_mb)
+        return effective_free, total_mb

And update the caller in tinyagentos/routes/models.py:

-                effective_free, _total = vram_mgr.available_vram()
+                effective_free, _total = await vram_mgr.available_vram()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/vram_reservation.py` around lines 165 - 191, available_vram() and
stats() still call _probe_vram() synchronously, which can block the event loop
when used from async paths like download_model. Update VRAMReservation so
available_vram() becomes async or reuses a cached probe result from reserve(),
and change the denial-path caller in models route to await/use the non-blocking
path; also review stats() to avoid direct synchronous probing or ensure it is
only used from non-async contexts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tinyagentos/vram_reservation.py`:
- Around line 165-191: available_vram() and stats() still call _probe_vram()
synchronously, which can block the event loop when used from async paths like
download_model. Update VRAMReservation so available_vram() becomes async or
reuses a cached probe result from reserve(), and change the denial-path caller
in models route to await/use the non-blocking path; also review stats() to avoid
direct synchronous probing or ensure it is only used from non-async contexts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a56d8d4-d207-4eb3-b2c4-891987a8c8da

📥 Commits

Reviewing files that changed from the base of the PR and between 9ac0ff7 and e4c3315.

📒 Files selected for processing (8)
  • scripts/install-rknpu.sh
  • tests/test_vram_reservation.py
  • tests/test_worker_manifest.py
  • tinyagentos/cluster/backend_services.py
  • tinyagentos/routes/models.py
  • tinyagentos/vram_reservation.py
  • tinyagentos/worker/agent.py
  • tinyagentos/worker/worker_manifest.py

@jaylfc
jaylfc merged commit 58cd159 into dev Jul 9, 2026
11 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in TinyAgentOS Roadmap Jul 9, 2026
jaylfc added a commit that referenced this pull request Jul 12, 2026
…1798)

Completes the remaining #1766 work after the #1767 hotfix (fail-open on
no-probe hardware, backend-level min_ram_mb gate, asyncio.to_thread probe).

- Reclaim VramReservation entries older than a configurable TTL (default 1h)
  so a hung installer cannot hold capacity until controller restart. Sweep
  runs from reserve(), available_vram(), stats(), and public sweep_stale().
- Extract _estimated_vram_mb() so the rkllama pull gate clearly reads
  requires.backends[].min_ram_mb (max across backends, variant fallback).
- Tests: no-probe + real backend min_ram proceeds (no 503), concurrent
  large reserves on NVIDIA still atomic, TTL reclaim, and route-level
  503 when free VRAM is measurable and insufficient.

Closes #1766
jaylfc added a commit that referenced this pull request Jul 15, 2026
* fix(projects): show consent-flow external agents in the External section (#1784)

Approved external CLI agents (grok, kilo) were appearing in the plain Members
list instead of under "External / Connected agents" next to the other connected
agents. The Members panel only classified a member as external when its
member_id matched a registry agent's handle, but the consent flow registers
these agents with an empty handle and adds the project member row keyed by the
canonical id. So the match never fired and they fell through to the main list.

Match external registry agents by canonical id as well as handle (older
identities like the assistant reference by handle, consent-flow agents by
canonical id), and map the "grok" framework, not only "grok-build", to the
Grok label so the badge reads correctly.

* test(secrets): add coverage for the Secrets app (#1785)

Covers the mount-time /api/secrets fetch and loading state, masked value
rendering, the empty and failed-fetch fallbacks, reveal and hide via the
per-secret API, add and delete through the dialog, and category filtering.
The GitHub integration is mocked so its on-mount identity fetch does not
interfere with the secrets assertions.

* test(notes): add vitest coverage for NotesApp/TodoApp mounted behavior (#1787)

Render NotesApp and TodoApp, mock the /api/notes fetch on mount, and assert real behavior: kind filtering, empty states, detail load on select, and the create flow.

* test(chess): add vitest coverage for ChessApp (#1788)

Cover render, legal moves, turn changes, checkmate status, new game reset,
and vs-agent mode, with the on-mount agents fetch mocked.

* test(imageviewer): add vitest coverage for ImageViewerApp (#1789)

Render the app and assert real behavior: empty state, file load,
zoom in/out with min/max clamps, 90-degree rotation, reset on new
image, and object URL revocation. Stub fetch and URL.createObjectURL
so on-mount integrations do not interfere.

* fix(desktop): Registry poll no longer resets scroll (#1761) (#1786)

* fix(desktop): keep Registry panel scroll stable across 5s polls (#1761)

Quiet background polls no longer flip loading (which unmounted the list)
and setEntries is a no-op when id/content are unchanged, so scroll and
in-progress interaction are preserved. Add registryEntriesEqual helper
and vitest coverage for the poll no-op path.

* fix(desktop): guard registryEntriesEqual index access and drop em dashes

The poll no-op comparison read a[i]/b[i] without a guard, which fails the
strict noUncheckedIndexedAccess build (spa-build). Add an explicit undefined
guard, and remove the em dashes from the added comments.

* chore(deps): bump the python-deps group with 3 updates (#1790)

Updates the requirements on [uvicorn[standard]](https://github.com/Kludex/uvicorn), [croniter](https://github.com/pallets-eco/croniter) and [litellm[proxy]](https://github.com/BerriAI/litellm) to permit the latest version.

Updates `uvicorn[standard]` to 0.51.0
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](Kludex/uvicorn@0.50.0...0.51.0)

Updates `croniter` from 6.2.3 to 6.2.4
- [Release notes](https://github.com/pallets-eco/croniter/releases)
- [Changelog](https://github.com/pallets-eco/croniter/blob/main/CHANGELOG.rst)
- [Commits](pallets-eco/croniter@6.2.3...6.2.4)

Updates `litellm[proxy]` to 1.92.0
- [Release notes](https://github.com/BerriAI/litellm/releases)
- [Commits](https://github.com/BerriAI/litellm/commits)

---
updated-dependencies:
- dependency-name: uvicorn[standard]
  dependency-version: 0.51.0
  dependency-type: direct:production
  dependency-group: python-deps
- dependency-name: croniter
  dependency-version: 6.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-deps
- dependency-name: litellm[proxy]
  dependency-version: 1.92.0
  dependency-type: direct:production
  dependency-group: python-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump the spa-deps group in /desktop with 16 updates (#1791)

---
updated-dependencies:
- dependency-name: "@codemirror/state"
  dependency-version: 6.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@codemirror/view"
  dependency-version: 6.43.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/extension-link"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/extension-underline"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/pm"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/react"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/starter-kit"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@types/three"
  dependency-version: 0.185.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: vite
  dependency-version: 8.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: vitest
  dependency-version: 4.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: spa-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* docs(design): account model, free username plus paid chosen subdomains (#1792)

* docs(design): Hailo-10H LLM backend, zero-touch install parity with RK3588 (#1793)

* docs(design): hub.taos.my local-first P2P social network foundation (#1794)

* feat(hailo): reserve port 7836 and map hailo-ollama llm-chat capability (#1795)

Slice 1 of the Hailo-10H LLM backend design (docs/design/hailo-llm-backend.md):
add 7836 to RESERVED_PORTS so apps cannot squat the NPU backend port, and
register hailo-ollama with llm-chat in BACKEND_CAPABILITIES. Closes the first
shippable piece of #1771.

* feat(account): controller proxy actions for subdomain check/claim/release (slice 3) (#1796)

* feat(account): proxy subdomain check/claim/release actions (slice 3)

Add /api/account/subdomains/{check,claim,release} routes to account_proxy.py
that forward to the taos.my subdomain claims service with the session cookie
passthrough. The name field is validated rid-style before it can reach the
upstream URL, so a crafted name cannot inject path/query (SSRF/path-traversal
guard). Implements account design doc slice 3.

* test(account): cover subdomain proxy forwarding, 503, and name validation

Add tests alongside the existing account_proxy suite: forwarding of check
(query name) and claim/release (body name) with cookie passthrough, 503 when
the account service is unconfigured, and 400 on an invalid name with no
upstream call.

* docs(design): Projects app nested elements, one project with typed elements (#1797)

* fix(models): VRAM reservation TTL sweep + #1766 acceptance coverage (#1798)

Completes the remaining #1766 work after the #1767 hotfix (fail-open on
no-probe hardware, backend-level min_ram_mb gate, asyncio.to_thread probe).

- Reclaim VramReservation entries older than a configurable TTL (default 1h)
  so a hung installer cannot hold capacity until controller restart. Sweep
  runs from reserve(), available_vram(), stats(), and public sweep_stale().
- Extract _estimated_vram_mb() so the rkllama pull gate clearly reads
  requires.backends[].min_ram_mb (max across backends, variant fallback).
- Tests: no-probe + real backend min_ram proceeds (no 503), concurrent
  large reserves on NVIDIA still atomic, TTL reclaim, and route-level
  503 when free VRAM is measurable and insufficient.

Closes #1766

* feat(account): frontend types + Account panel split for username/subdomains (slice 4) (#1801)

Implements slice 4 of docs/design/account-username-subdomain-model.md.

- account-client: add SubdomainClaim/SubdomainCheck types, Account.username
  and Account.subdomains, deprecate Account.handle; add checkSubdomain,
  claimSubdomain, releaseSubdomain helpers with the same degrade-to-state
  (AuthError, never throw) error style as the auth actions.
- AccountPanel: split the old ReserveHandleCard into a free UsernameCard
  (no taOSgo mention, no .taos.my suffix) and a SubdomainsCard (claim list
  with active/grace badges, inline availability check, release, disabled claim
  UI when unsubscribed). Update the section intro copy.
- Tests: account-client subdomain helper coverage (mocked website endpoints);
  AccountPanel coverage for free-username copy, claim list rendering, disabled
  claim when unsubscribed, and grace badge.

* feat(hailo): slice 2 hailo-ollama installer (#1771) (#1803)

* feat(hailo): slice 2 hailo-ollama installer (#1771)

Implements docs/design/hailo-llm-backend.md slice S2 (section C):
scripts/install-hailo.sh mirrors scripts/install-rknpu.sh structure and
safety contract. Detects Hailo-10H via /dev/hailo0 + lspci/hailortcli,
installs HailoRT (>= 5.1.0 firmware floor) on Raspberry Pi OS, clones
hailo-ollama at a pinned ref remapped to port 7836, installs a systemd
unit with orphan-reap ExecStartPre, and health-waits on /api/tags.

Verification: bash -n, shellcheck, and a non-Hailo host prints the
no-detection notice and exits 0 without touching the system.

* chore(hailo): doc-gate trailer for install-hailo.sh

The installer is specified line by line in docs/design/hailo-llm-backend.md
section C (slice S2), which is already merged on dev.

Docs-Reviewed: implements the merged design doc docs/design/hailo-llm-backend.md section C, no separate doc change needed

* feat(hailo): slice 3 hailo-ollama managed service manifest (#1804)

* feat(hailo): slice 3 hailo-ollama managed service manifest

Add app-catalog/services/hailo-ollama/manifest.yaml per the managed-backend
contract (lifecycle.auto_manage, unit, scope=system, health on 7836). The
backend flows through load_managed_backends() with no new plumbing.

Adds a unit test that loads the real manifest and asserts the backend is
returned by load_managed_backends().

Docs-Reviewed: implements the merged design doc docs/design/hailo-llm-backend.md

* fix(hailo): declare the proprietary license in the hailo-ollama manifest

test_services_manifests requires a license key on every service manifest.
The runtime is Hailo proprietary software behind an install-time EULA
acceptance, so the manifest now says exactly that.

Docs-Reviewed: license posture is specified in docs/design/hailo-llm-backend.md security and licensing section

* feat(account): onboarding free username claim step (slice 5) (#1805)

Adds a free taOS username claim step to OnboardingScreen, shown right
after local account creation. Clearly labeled free, never gated behind
taOSgo, and the user can always finish (claim is optional and failures
degrade to a Settings pointer). Public subdomain publishing is deferred
to Settings so onboarding never dead-ends on the paid path.

Bounded to OnboardingScreen.tsx plus its test, per the slice plan.

* feat(hub): identity keypair keystore + directory registration proxy (slice 1) (#1806)

Implements slice 1 of the hub.taos.my own-your-posts social network design
(docs/design/hub-social-network-foundation.md).

Controller side, the taos.my directory endpoints are the contract (mocked in
tests):

- tinyagentos/hub/identity.py: node keystore that mints an Ed25519 signing key
  and an X25519 encryption key on first use, persists them 0600 under
  <data_dir>/hub/identity.json (mesh_credentials.py pattern: atomic write,
  allowlisted fields, TAOS_DATA_DIR override), and exposes the registerable
  public view, the SHA-256 author fingerprint, and a challenge-proof signer plus
  verifier.
- account_proxy.py: _ACTIONS additions and same-origin routes for hub identity
  register / lookup / rotate, forwarding to /api/hub/identity/* with session
  cookie pass-through; lookup validates the username as an rid-style token before
  it can reach the upstream URL.

Tests: keystore round-trip + 0600 perms + stable fingerprint, proxy forwarding +
503 (unconfigured and unreachable), challenge proof rejects a wrong key, lookup
relays the append-only key log.

* feat(hailo): slice 4 install-time gates for Hailo-10H (per design doc) (#1807)

Mirror the Rockchip RKNPU install-time gates for the Hailo-10H NPU across
install.sh, scripts/install-server.sh and scripts/install-worker.sh:
detect /dev/hailo0 with 10H vs 8L discrimination (lspci/hailortcli, with
TAOS_FORCE_HAILO override), chain into scripts/install-hailo.sh under
TAOS_HAILO_SETUP=1, fail-soft on chain failure, and document the new env
vars in each script header. Part of #1771.

Docs-Reviewed: implements the merged design doc (doc-gate requires it)

* feat(hailo): slice 5 runtime detection + provider adapter for Hailo-10H (per design doc) (#1808)

Add the hailo-ollama backend end to end with no new hardware needed for CI:
- worker probe candidate on the taOS remap port 7836, Ollama-compatible
- OllamaCompatAdapter entry for hailo-ollama
- litellm_config ollama-compat membership extended to hailo-ollama
- provider type registered so auto_register_from_manifest seeds
  local-hailo-ollama on Hailo-10H hardware (mirrors rkllama local-rkllama)
- tests for detection, LiteLLM ollama/<model> prefix, and seed

* feat(hub): profile object + local hub store (slice 2) (#1809)

* feat(hub): profile object + local hub store (slice 2)

Implements slice 2 of the hub.taos.my own-your-posts social network design
(docs/design/hub-social-network-foundation.md).

- tinyagentos/hub/store.py: canonical-JSON encode/hash/sign/verify helpers and
  a SQLite HubStore (objects, blobs, authors tables). Objects are canonical-JSON
  encoded, content-addressed by SHA-256 of the canonical bytes excluding the
  signature, and signed by the slice-1 Ed25519 keystore. Profiles are the one
  mutable object with highest-version-wins semantics; put_profile ignores a
  stale (lower-or-equal version) replica so it can never clobber a newer one.
- tinyagentos/routes/hub.py: local API the Hub app consumes. Render the node's
  own profile and create/update it with a version bump, each response carrying
  an explicit degrade state (no-identity / no-profile / ok). The store is opened
  lazily and colocated with the identity keystore under the data dir. Wired into
  routes/__init__.py. No peer networking; directory calls stay in account_proxy.

Tests: canonicalization vectors, sign/verify (good sig verifies, tamper and
wrong key do not), version-wins, object/blob/author store round-trip, and the
profile routes end to end (degrade states, create + version bump, kind
validation, signature check).

* chore(hub): doc-gate trailer for the hub store slice

The profile object and local hub store are specified in
docs/design/hub-social-network-foundation.md (slice 2), merged on dev.

Docs-Reviewed: implements the merged design doc docs/design/hub-social-network-foundation.md slice 2, no separate doc change needed

* feat(hub): follow / friend / circle model + request brokering (slice 3) (#1810)

Implement hub social slice 3 from docs/design/hub-social-network-foundation.md:
- signed follow and cache-grant statements (cache-grant stored, not yet
  acted on; the cache worker lands in slice 6),
- friend-request send/accept/decline flows that broker through the
  directory and record the local accepted edge,
- local block (severs every edge to the peer and asks the hub to revoke
  the server-side edge) and mute operations,
- a presence gate that denies lookup without an accepted edge,
- directory proxy entries for requests, presence, and edge revoke.

Tests cover edge authorization (presence denied without an accepted edge),
rate-limit behavior, and block severing the edge.

Docs-Reviewed: implements the merged design doc (doc-gate requires it)

* feat(projects): nested element store, CRUD routes, and task element tags (slice 1) (#1811)

* feat(projects): implement nested element store, CRUD routes, and task element tags (slice 1)

Adds project elements (one level of nesting per the design) with a dedicated
store and owner-gated CRUD routes, an element_id tag on tasks with create and
update validation plus list and ready filtering, and the Beads snapshot
carrying the tag. Group/promote and assignment are later slices.

Docs-Reviewed: implements the merged design doc docs/design/projects-nested-elements.md slice 1.

* test(projects): slice 1 element store, CRUD route, and task tag coverage

Adds the element store unit tests and route-level coverage for element CRUD,
tag validation, the 409 delete guard, untag mode, and element_id filtering on
list/ready. Proves an external agent token filters by element with no auth
change.

Docs-Reviewed: implements the merged design doc docs/design/projects-nested-elements.md slice 1.

* feat(projects): kanban element filter bar (slice 2) (#1812)

Add a persistent element axis to the kanban board: element client API and
element_id on task types, a pure element filter in boardFiltering, a new
ElementFilterBar rendered from the toolbar (All | element chips | Project-
level), an element badge on cards when the board is unfiltered, and element
fetching wired through useBoardData. Zero-element projects stay untouched
(the bar does not render). Tests added and existing board tests updated.

Docs-Reviewed: implements the merged design doc (doc-gate requires it for scripts/, app-catalog/, tinyagentos/ changes)

* feat(hub): post objects, chain logic, image ingest, composer + own-timeline (slice 4) (#1813)

Implements slice 4 of docs/design/hub-social-network-foundation.md: a per-author
hash chain (seq/prev), signed append plus verify and tamper detection, signed
tombstones that drop content while keeping the chain verifiable, and image
ingest that re-encodes and strips EXIF. Adds the local post, timeline, and
delete routes plus the Hub app: a composer with a loud friends-only-by-default
visibility switch and an own-timeline read from the local store. No peer sync
yet (that is slice 5).

Tests cover chain append/verify, tamper detection, tombstone drops content and
keeps the chain verifiable, and EXIF stripped.

Docs-Reviewed: implements the merged design doc (doc-gate requires it for scripts/, app-catalog/, tinyagentos/ changes)

* fix: map violet and red note colors to valid tldraw palette names (#1815)

Canvas notes with payload.color violet or red fell back to yellow
because the tldraw COLOR_MAP in NoteShape.tsx was missing those
entries. Added violet, red, and light-blue mappings so the note
shape renders with the correct background color. Added tests to
verify the color strings survive element-to-shape coercion and
that COLOR_MAP has the expected entries.

* docs(readme): External Coding Agents section (bring your own AI team) (#1816)

* docs(readme): External Coding Agents section (registry, consent onboarding, kanban + a2a work loop)

The external-agent collaboration flow (access requests approved from the
phone, scoped registry identities, board claim/PR/close loop, a2a
coordination) had no README presence despite being live and proven.

Docs-Reviewed: readme-only change describing the shipped flow documented in docs/design/external-agent-onboarding.md

* docs(readme): reference only docs that exist on dev

doc-gate verifies every mentioned path exists; the project-invite design
doc lives on a branch, so the section links only the onboarding doc.

Docs-Reviewed: readme-only change describing the shipped flow documented in docs/design/external-agent-onboarding.md

* fix(hub): serialize chain appends so racing posts are not orphaned (#1817)

next_chain_position read the chain head and put_chain_object inserted with
INSERT OR IGNORE, so two concurrent appends for the same author computed the
same seq and the loser was silently dropped from the chain index while its
body stayed in hub_objects. A per-store asyncio lock now serializes the
read-position-then-insert section in append_post and delete_post; the local
node is the only writer of its own chain, so this closes the race. Regression
test proves three concurrent appends land as seq 1,2,3.

Docs-Reviewed: hardening of the merged design doc docs/design/hub-social-network-foundation.md slice 4, no doc change needed

* fix(canvas): map text elements to visible taos-text shapes (#1819)

* fix(canvas): map text elements to visible taos-text shapes

element-to-shape.ts only mapped note/link/image to custom shape types;
text (and mermaid label) fell through to taos-generic whose props only
carry geometry, so the payload never reached a visible label. Add a
taos-text shape util and map kind=text to it, coercing payload.text to
a string with empty-string default so imperfect agent writes still render.

Add tests for text kind mapping, payload coercion, and fallback behavior.

* chore(canvas): doc-gate trailer for text shape

New desktop source (TextShape.tsx) rendering the canvas text kind; no
behavioral doc needed, the canvas element kinds are covered by the design.

Docs-Reviewed: frontend-only canvas fix for the boarded canvas text render bug, no doc change needed

* feat(projects): element overview grid, creation flow, and drill-in navigation (slice 3) (#1820)

* feat(projects): element overview grid, creation flow, and drill-in navigation (slice 3)

Implements slice 3 of docs/design/projects-nested-elements.md on the frontend:

- New elements/ registry (types.ts) with the seven known types, their
  icons, and type-driven landing-tab order.
- ElementGrid: overview grid shown when a project has elements, with a
  fixed Project card and an Add element tile; zero-element projects keep
  today's workspace pane untouched (back-compat invariant).
- ElementCard: type icon, name, type label, open/total task counts, owner
  chip, and a recent-activity line.
- ElementCreateDialog: create a single nested element (name, slug, type,
  optional owner) within an existing project.
- CreateProjectDialog: optional second step to seed nested elements;
  skipping yields a project identical to today's.
- ProjectWorkspace: element drill-in scopes the board to the element and
  lands on the type's preferred tab, with a breadcrumb and the element id
  carried on the URL for deep links.
- ProjectBoard/BoardToolbar: accept a scoped element id and hide the
  element filter bar while scoped.

Tests cover the grid, card, create dialog, the zero-element regression,
drill-in with breadcrumb, and the creation-flow step two.

* chore(projects): doc-gate trailer for elements slice 3 UI

New desktop sources under ProjectsApp/elements/ implementing slice 3 of the
merged nested-elements design; frontend-only, no behavioral doc change.

Docs-Reviewed: implements the merged design doc docs/design/projects-nested-elements.md slice 3

* Add confirm guard before video delete (#1821)

Docs-Reviewed: frontend change to the shipped Video Studio surface.

* Projects elements slice 4: element-scoped canvas + files (#1822)

Add an element_id tag to canvas items (store column + ALTER migration,
element-filtered list and create on the canvas routes), an adopt-existing
element files subfolder helper, untag-on-delete for canvas items, and wire
the frontend so the canvas honors the active element filter and the Files
tab mounts the element subfolder.

Docs-Reviewed: implements the boarded task, frontend/backend change to shipped surface.

* feat(projects): doc-review stamp store + routes (#1802 slice 3)

Add per-document review_state machine (awaiting_review/approved/changes_requested)
with actor recording and timestamps, project-scoped agent-token gated routes, and
the desktop stamp badge column plus typed API client.

Docs-Reviewed: doc-review stamp store + routes slice per document-review-surface.md

* feat(projects): add doc-review stamp badge to FilesApp and fix projects.ts types

- Add ReviewBadge component with onClick support for cycling review state
- Add cycleDocReview callback for in-place state transitions
- Wire badge into FileRow (list view) and grid cards (project: locations only)
- Fetch review states on project-location navigation via projectsApi.docReviews.list
- Remove duplicate DocReviewState/DocReview type definitions from projects.ts
- Remove duplicate docReview API block; keep single canonical docReviews namespace
- Use DocReview | DocReviewMissing union for GET response type
- Apply encodeURIComponent to state filter query parameter

Docs-Reviewed: doc-review stamp store + routes slice per document-review-surface.md

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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