Skip to content

release: promote dev to master (v1.0.0-beta.48) - #2365

Closed
jaylfc wants to merge 84 commits into
masterfrom
dev
Closed

jaylfc wants to merge 84 commits into
masterfrom
dev

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Promotes dev to master for v1.0.0-beta.48 (release PR #2364, 27 PRs since beta.47). Closes out the tsk-laezfg / #2362 release-time verification: the secret-ignores-gate runs on this PR's merge result and the master push, proving the promoted .gitignore keeps every secret-material rule. Post-merge checks: tree identity (diff dev..master empty) before tagging v1.0.0-beta.48.

Summary by CodeRabbit

  • New Features

    • Added five Hailo-10H model options, increasing the catalog to 120 manifests.
    • Added editable agent handles for owners and administrators.
    • Added wallpaper fitting options per device: cover, contain, stretch, centered, and tiled.
    • Added task quarantine and lead-authorized unquarantine controls with strike tracking.
    • Improved agent chat responsiveness with queued messages, background work, progress, and status reporting.
    • Added external access for supported OpenAI-compatible model endpoints.
  • Bug Fixes

    • Improved security around session binding, memory paths, and unauthorized resource visibility.
    • Fixed project reorder rollback and task state handling.
  • Documentation

    • Updated release, model catalog, API, agent, and deployment documentation.

jaylfc and others added 30 commits August 9, 2026 15:19
Design spec for AI-assisted SDR/radio analysis studio (Sparky pattern).
Covers studio app layout, rf-scanning skills, Hermes/ARM prism blocker,
SDR USB passthrough, receive-only defaults, and Phase 1 cut.
Radio Studio - SPEC: AI-assisted SDR/radio analysis studio (Sparky pattern)
…#2288)

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

* 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](https://github.com/Kludex/uvicorn/compare/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](https://github.com/pallets-eco/croniter/compare/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>

* fix(installer): normalise tree ownership before re-run update (#1840)

A re-run of install-server.sh over an existing checkout drops to the
repo-owning user (the 'taos' service user) for the git fetch + reset, to
avoid running git as root inside a user-writable tree. But it only reads
the TOP-LEVEL dir owner. If a prior install was interrupted mid-chown (or
a root step wrote a few paths back), the tree has MIXED ownership: the
owning user then cannot unlink the still-root-owned paths, so the reset
fails with 'unable to unlink old ...: Permission denied' ->
'Could not reset index file to revision origin/master', bricking every
subsequent re-run.

Normalise ownership to the owning user (chown -R, run by root) right
before the update so the reset can rewrite the whole tree. Safe: root
does the chown and git still runs unprivileged.

Reported on #2 (fresh Orange Pi 5 Plus, retry after a partial first run).
Failure class reproduced locally: an unwritable path in the tree yields
the identical unlink-EACCES; normalising the tree makes the reset apply
cleanly.

* fix(projects): create element_id indexes after migration, not in SCHEMA (boot-brick) (#1853)

The canvas and task stores put their element_id index in SCHEMA:
  CREATE INDEX ... ON project_canvas_elements(project_id, element_id)
  CREATE INDEX ... ON project_tasks(project_id, element_id)
BaseStore runs SCHEMA (executescript) BEFORE _post_init, so on an existing
pre-element_id database the index creation raised
'no such column: element_id' before the _post_init ALTER could add the column,
crashing controller boot after an upgrade. Reproduced live: the Pi bricked on
startup right after pulling this code (fresh installs were fine because SCHEMA
creates the table WITH element_id, so CI never exercised the migration path).

Move both indexes out of SCHEMA into _post_init, created after the ALTER.
Same class of bug and fix as the registry active-handle index (#1841).
Regression test seeds a pre-element_id DB and asserts both stores boot,
migrate the column, and build the index.

* fix(hailo): install from hailo_model_zoo_genai via cmake, not a nonexistent repo (#1851)

The installer defaulted HAILO_OLLAMA_REPO to hailo-ai/hailo-ollama, which does
not exist (git ls-remote: Repository not found), so every Hailo-10H install
failed at clone. The Hailo-Ollama server is not a standalone repo: it ships
inside hailo-ai/hailo_model_zoo_genai and is built from source there (its
README: an Ollama-compatible API written in C++ on top of HailoRT).

- Point the repo at hailo_model_zoo_genai, pinned to a real commit.
- Replace the Python venv/pip build with the repo's actual cmake flow
  (configure -> build -> install), adding the C++ toolchain + OpenSSL deps.
  cmake --install lands the hailo-ollama binary in /usr/local/bin and manifests
  under /usr/local/share/hailo-ollama.
- Resolve the binary from PATH (system install), not a venv.
- Start the server bare (it has no serve subcommand or --port flag) and set the
  listen port via OLLAMA_HOST (bind 127.0.0.1 on the managed port 7836, off the
  banned default 8000), per the repo's docs/USAGE.rst.

Not hardware-tested locally (no Hailo-10H here); validated by bash -n and the
upstream build docs. doc62fr (#1771) tests live on real hardware and reports logs.

* chore(deps): bump mcp in the uv group across 1 directory (#2010)

Bumps the uv group with 1 update in the / directory: [mcp](https://github.com/modelcontextprotocol/python-sdk).


Updates `mcp` from 1.27.2 to 1.28.1
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.27.2...v1.28.1)

---
updated-dependencies:
- dependency-name: mcp
  dependency-version: 1.28.1
  dependency-type: indirect
  dependency-group: uv
...

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

* chore(deps): bump immutable (#2090)

Bumps the npm_and_yarn group with 1 update in the /desktop directory: [immutable](https://github.com/immutable-js/immutable-js).


Updates `immutable` from 4.3.8 to 4.3.9
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v4.3.8...v4.3.9)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 4.3.9
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

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

* release: promote dev to master for v1.0.0-beta.44 (#2122)

* fix(desktop): dialogs render above windows, and mint shows the URL and PIN (#2092)

Two bugs on the same screen, both reported from live use.

Window z-index was an unbounded counter: every open, focus, restore and
recenter incremented nextZIndex forever, while portal overlays sit at a fixed
z-[10001]. After enough focus switches in one session, windows rendered on top
of modal dialogs. The stack is now renumbered to 1..N on each change, so window
z stays far below the overlay layer regardless of session length, and relative
order is preserved by sorting on the existing values first. This affected every
portal overlay, not only the invite dialog.

ProjectMembers closed the invite dialog in its onMinted handler, unmounting it
before the result rendered. The invite URL and PIN are shown exactly once and
cannot be recovered, so a successful mint looked like a silent failure. The
parent now refreshes its member list only and the user closes the dialog once
they have copied the credentials.

* feat(agents): project_tasks_create scope so an external agent can author cards (#2098)

An agent holding project_tasks could claim, close and comment on existing
cards but never open one, so an approved grant bought nothing on that route.
Rather than widen project_tasks, which is documented and tested as read plus
lifecycle plus comments (Invariant 2 + 5) and would retroactively grant
authoring to every agent already approved for it, authoring gets its own
narrower scope that an owner opts into per agent.

create_task now authorises through the same _authorize_task_actor as the other
task routes, parameterised on scope, so existence-hiding 404s behave
identically. The middleware allowlist admits POST .../tasks, which lets the
token reach a handler that then verifies JWT, project binding and the narrower
scope; project_tasks alone is still refused.

Tests keep the original invariant (project_tasks alone cannot create) and add
the halves that make it meaningful: the new scope DOES allow authoring, it is
project-bound so a grant on A cannot create on B, and it does not widen member
management. Authorship is attributed to the agent, not the project owner.

* feat(library): item card component with thumbnail, status, artifacts, collection link (#2097)

Add LibraryItemCard component per docs/design/library-app.md sections 2-4.
Card shows thumbnail (or placeholder), title, kind badge, media duration,
pipeline status per stage (jobs shape), artifact list (text, transcript,
description, ocr) with preview, link-to-collection action, and a disabled
Download stub until P3. Failure states are always visible -- no silent
empties for missing thumbnails, pipeline stages, artifacts, or errors.

Includes lib/library.ts with types and API client for the library store
(items, artifacts, jobs) and 25 component tests covering pending,
processing, ready, and error states.

* feat(agents): enforce files_read/files_write so member agents can access project Files (#2100)

* feat(agents): enforce files_read/files_write scopes so member agents can access project Files

Project-files routes (/api/projects/{slug}/files*, mkdir, trash, stats) had no
membership or scope gate and were absent from the agent middleware allowlist, so
an agent token could not reach them at all while the files_read/files_write
scopes existed but were never enforced. This wires them up, mirroring the canvas
pattern:

- _authorize_files_actor resolves slug -> project and authorizes a session
  owner/admin (unchanged) OR an agent holding files_read (reads) / files_write
  (writes) grant bound to that project. A token bound to another project, or an
  unknown slug, collapses into an existence-hiding 404; a missing scope is 403.
- _AGENT_FILES_ROUTES added to auth_middleware so agent JWTs pass through to
  the routes, which verify the grant.
- InviteAgentDialog offers files_read (default on) and files_write, so an owner
  can grant file access at invite time.

Agents that are project members can now read the project's Files and add files
via the API. Grant creation already grants these scopes generically, and
membership is added via the always-on project_tasks scope.

Adds tests/test_routes_project_files_agent_scope.py (10 cases covering read/write
allow, missing-scope 403, cross-project 404, unknown-slug 404, session owner
unchanged).

* feat(agents): surface Files in the invite bundle + fix agent API-surface docs

- build_connection_bundle now advertises the project Files endpoints and adds a
  Files capability section to the join guide when files_read/files_write are
  granted, plus task_create when project_tasks_create is granted, so a joining
  agent is told the Files API exists and how to reach it (slug-keyed paths).
- docs/agent-coordination.md: drop the non-existent project_doc_review scope,
  add files_read/files_write, project_tasks_create, and decisions_write to the
  agent API-surface list, and correct the doc-gate note (it fires only on file
  add/delete, so it does not catch allowlist edits).
- README: replace the understated read-only agent-surface sentence with the
  real scoped surface (tasks, canvas, files, decisions, a2a).

Verified against VALID_SCOPES / _ALLOWED_SCOPES and the auth_middleware
allowlist. Invite tests pass (36).

* feat(providers): add Nous Portal as a cloud model provider (#2102)

Nous Portal (Nous Research) is an OpenAI-compatible inference API serving the
Hermes 4 family and frontier models. Wire it up as a first-class cloud provider
so it can be added from the Providers app instead of a hand-configured
openai-compatible endpoint.

- providers/__init__.py: add 'nous' to ALL_TYPES + CLOUD_TYPES, and map it to
  the OpenAI LiteLLM prefix (api_base set explicitly, like kilocode).
- routes/providers.py: default base URL https://inference-api.nousresearch.com/v1
  and a seed model list (flagship Hermes models) for the case where /v1/models
  cannot be listed without a working credential.
- backend_adapters.py: 'nous' uses the CloudAPIAdapter probe.
- Frontend: add 'nous' to the cloud provider type lists and the Providers app
  metadata (label 'Nous Portal', default URL, description, key placeholder).

Base URL and OpenAI-compatibility verified against Nous Portal docs. Backend
provider suite passes (68); frontend tsc clean.

* feat(desktop): Assistant Studio - a workspace for a personal-assistant agent (#2103)

A new studio app where the user picks a registered agent to be their PA and
works out of one hub. Left rail: Overview, Journal, Calendar/time, Tasks, Comms,
Canvas, and a Deliverables (files/reports) area. The PA picker defaults to
Hermes when present and persists the choice. Journal, Tasks, Calendar events and
Deliverables persist locally per PA so switching PA swaps the whole workspace;
Comms opens the live agent chat and Canvas points at the project canvas.

MVP scope: self-contained, no new backend (localStorage-backed), so it is
additive and safe. Accessible (labels, aria-current, keyboard add). Registered
as an optional studio app. Backend wiring (real calendar, PA-scoped board/files)
is a follow-up.

tsc clean; frontend build passes.

* feat(agents): request additional scopes for an existing agent identity (#1921)

* feat(agents): request additional scopes for an existing agent identity

Add a scope-request flow so an already-registered agent can gain more
scope grants on its SAME canonical_id, instead of the auth-request flow
which mints a new identity on approval (and 409s on an active-handle
collision).

Endpoints (in routes/agent_auth_requests.py):
- POST /api/agents/registry/{cid}/scope-requests            (create)
- POST /api/agents/registry/{cid}/scope-requests/{id}/approve
- POST /api/agents/registry/{cid}/scope-requests/{id}/deny

Auth (security-critical): creation is gated to the agent's OWN registry
bearer token (sub == canonical_id) OR the owning user / an admin, because
the agent already holds credentials; an anonymous caller can never
escalate an existing identity. The middleware allowlist exposes only the
create path to a registry JWT; approve/deny are owner/admin only.

Approval writes add_grant(cid, scope, project_id) per granted scope
(idempotent via the UNIQUE key), never registers a second identity, and
lets the admin narrow but not widen the requested scopes.
decisions_read/decisions_write are grantable globally or per-project;
project_tasks and canvas scopes still require an explicit project_id.

Adds AgentScopeRequestsStore, a scope-agnostic check_agent_identity
helper, and full route tests. VALID_SCOPES stays in sync with
_ALLOWED_SCOPES.

Fixes #1920

* fix(agents): fold scope-request approval security findings (#1921)

Addresses the Kilo + CodeRabbit findings on the approve/create scope-request paths:

- Major (project binding): approve_scope_request bound global-capable scopes
  (decisions_*) to effective_project, which fell back to the agent-named
  req.project_id when the operator gave no explicit project_id. Since an agent
  can self-request, that let a global scope bind to any project the operator
  never validated (cross-project escalation). Now grants bind ONLY to the
  operator's explicit body.project_id (None = global); the agent-named value is
  never a binding.
- Atomicity + races: approve_scope_request wrote grants + membership before the
  set_decision flip with no lock, so concurrent approvals could double-grant.
  Wrapped the whole approval in the same per-request _get_approve_lock the
  consent path uses, with a pending re-check inside the lock (grant-before-flip
  is safe under the lock + idempotent add_grant).
- Info leak: create_scope_request now authorizes BEFORE scope-vocabulary
  validation, so an unauthorized caller cannot probe whether a scope name is
  valid.

Adds tests: global scope ignores an agent-supplied project_id (binds global),
and create authorizes before vocab (403 not a 400 vocab leak). 16 tests pass.

* fix(agents): take the per-request lock in deny_scope_request too (#1921)

Kilo review: approve_scope_request now serializes concurrent approvals under
_get_approve_lock; deny lacked the same lock, so a concurrent approve+deny of
the same request was unserialized. Wrap the deny body in the same per-req lock
with a pending re-check, matching approve and the sibling consent path.

* fix(apps): register assistant-studio as an installable optional app (#2104)

Assistant Studio (#2103) shipped in the frontend registry as optional but was
not in the server-side optional-app catalog, so getLaunchableApps hid it (it is
only shown once installed) and POST /api/apps/optional/assistant-studio/install
returned 'not an optional app'. Add it to OPTIONAL_FRONTEND_APPS + the version /
trust / provenance dicts, matching the other Creative Studios.

* fix(shortcuts): resolve the container's real incus project (and start it) for terminal shortcuts (#2105)

The agent terminal/TUI shortcuts opened an incus PTY with no --project flag, so
incus used the client's default project (a per-user one like user-999). An agent
whose container lives in a different project (e.g. a legacy container in
'default') failed with 'Failed to fetch instance taos-agent-<name> in project
user-999: Instance not found', even though the container exists. Every other
container op already resolves the real project via _resolve_container_project
(--all-projects); the PTY path was the lone exception.

_open_incus_pty now resolves the container's actual project and passes
--project, and starts the container if it is stopped (incus exec fails on a
non-running instance), so a dev-access shortcut works regardless of project or
run state. Adds a sync _resolve_project_and_state_sync sibling to the async
resolver.

Tests: exec targets the resolved project + stopped container is started; a
running container is not restarted.

* test(scope-requests): add grant-enforcement E2E, tighten approve tolerance, add deny negative test (#2108)

Three test gaps filled following PR #1921 merge:

1. E2E grant-enforcement test (test_approved_scope_grant_unlocks_route_e2e):
   agent self-requests decisions_write → admin approves → agent uses token
   on POST /api/decisions → assert 200. Proves the full grant-enforcement
   chain is wired end to end. Regression guard for issue #2095.

2. Tighten test_agent_cannot_approve_its_own_request:
   replace (401, 403) tolerance with exact 401. The middleware does not pass
   a registry JWT through to the approve handler (only the create path is
   allowlisted), so it falls to the session gate → 401 exactly.

3. Deny negative test (test_agent_cannot_deny_its_own_request):
   same auth model as approve — middleware does not allowlist the deny
   endpoint for registry JWTs, so an agent token on the deny endpoint
   falls through to the session gate → 401.

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>

* chore(deps): bump actions/setup-python from 6 to 7 (#2109)

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* chore(deps): bump the python-deps group with 2 updates (#2110)

Updates the requirements on [matrix-nio](https://github.com/matrix-nio/matrix-nio) and [litellm[proxy]](https://github.com/BerriAI/litellm) to permit the latest version.

Updates `matrix-nio` from 0.25.2 to 0.26.0
- [Changelog](https://github.com/matrix-nio/matrix-nio/blob/main/CHANGELOG.md)
- [Commits](https://github.com/matrix-nio/matrix-nio/compare/0.25.2...0.26.0)

Updates `litellm[proxy]` to 1.93.0
- [Release notes](https://github.com/BerriAI/litellm/releases)
- [Commits](https://github.com/BerriAI/litellm/compare/v1.92.0...v1.93.0)

---
updated-dependencies:
- dependency-name: matrix-nio
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-deps
- dependency-name: litellm[proxy]
  dependency-version: 1.93.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(release): v1.0.0-beta.44 version bump + changelog (#2121)

* chore(release): v1.0.0-beta.44 version bump + changelog

* docs(changelog): complete the beta.44 entry (dialog/mint fix, task-create scope)

* test(desktop): add unit tests for AssistantStudioApp (#2116)

* test(desktop): add unit tests for AssistantStudioApp

* test(desktop): await the mount-time agents fetch in the first two Assistant Studio tests

Qodo review: the first two tests rendered the component without awaiting its
mount-time /api/agents fetch, so the resulting setState could land outside
React Testing Library's act() and flake in stricter environments. The later
tests in the same file already waited, so this was an inconsistency as much as
a latent flake. Both now drain the fetch before asserting.

* fix(library): wire up the unused source ingest option (#2117)

* fix(library): wire up the unused source ingest option

* fix(library): remove the unused source ingest option instead of sending it

Review found that serializing source only moved the silent drop server-side:
/api/library/ingest accepts file, url and title only, and LibraryStore has no
source column (its source_url is already derived from the url). So the field was
discarded either way, while now looking wired.

The card offered wire-it-in or remove-it and I recommended wire-it-in without
checking the backend, which was wrong. Removing it is the honest fix: no caller
passes source, so nothing breaks, and a dead option no longer implies a feature
that does not exist. Capturing real source metadata is a backend feature, not a
client nit.

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

Bumps the spa-deps group in /desktop with 17 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.19` | `1.1.23` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.20` | `2.1.24` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.11` | `2.1.15` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.3` | `2.3.7` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.3.0` | `1.3.3` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.3` | `1.3.7` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.17` | `1.1.21` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.12` | `1.2.16` |
| [@tiptap/extension-link](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-link) | `3.28.0` | `3.29.0` |
| [@tiptap/extension-underline](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-underline) | `3.28.0` | `3.29.0` |
| [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) | `3.28.0` | `3.29.0` |
| [@tiptap/react](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/react) | `3.28.0` | `3.29.0` |
| [@tiptap/starter-kit](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/starter-kit) | `3.28.0` | `3.29.0` |
| [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.2.7` | `19.2.8` |
| [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.2.7` | `19.2.8` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.61.1` | `1.62.0` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.3` | `6.0.4` |


Updates `@radix-ui/react-dialog` from 1.1.19 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.20 to 2.1.24
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-label` from 2.1.11 to 2.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-select` from 2.3.3 to 2.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-slot` from 1.3.0 to 1.3.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.3.3 to 1.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.17 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-tooltip` from 1.2.12 to 1.2.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

Updates `@tiptap/extension-link` from 3.28.0 to 3.29.0
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-link/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/extension-link)

Updates `@tiptap/extension-underline` from 3.28.0 to 3.29.0
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-underline/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/extension-underline)

Updates `@tiptap/pm` from 3.28.0 to 3.29.0
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/pm/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/pm)

Updates `@tiptap/react` from 3.28.0 to 3.29.0
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/react/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/react)

Updates `@tiptap/starter-kit` from 3.28.0 to 3.29.0
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/starter-kit/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/starter-kit)

Updates `react` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react)

Updates `react-dom` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react-dom)

Updates `@playwright/test` from 1.61.1 to 1.62.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.1...v1.62.0)

Updates `@vitejs/plugin-react` from 6.0.3 to 6.0.4
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.4/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.23
  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.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.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.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/extension-link"
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: "@tiptap/extension-underline"
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: "@tiptap/pm"
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: "@tiptap/react"
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: "@tiptap/starter-kit"
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: react
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: react-dom
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@playwright/test"
  dependency-version: 1.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: spa-deps
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.4
  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>

* fix(agents): bind project_tasks_create and files scopes to a project on approval (#2127)

* fix(agents): bind project_tasks_create and files scopes to a project on approval

Review of the beta.44 promotion found that _SCOPE_PROJECT_SCOPES listed only
project_tasks and the canvas scopes, so project_tasks_create, files_read and
files_write could be approved with no project_id. The grant was then written
global (project_id=None), and check_agent_scope_for_project only matches a grant
bound to the project, so the operator believed they had granted access while the
agent silently had none. Fails closed, but silently wrong is its own bug.

Also replaces the em dashes in docs/agent-coordination.md with commas and colons
per the house style, and records in project_files.py why the session path
deliberately allows an unknown slug (lazily-created, slug-addressed files tree,
documented by test_list_unknown_slug_returns_empty) while the agent path stays
strict.

42 tests pass (project files, files agent scope, scope requests).

* fix(projects): setting an agent as project lead now sets lead_member_id (#2113)

add_agent_to_project wrote role='lead' on the member row but never called
set_lead, which is the only writer of projects.lead_member_id. The member row
is just a label; the pointer column is the actual lead. So the agent read as
lead in the UI while every lead-gated check refused them. This is what happened
to Hermes on taOSrabbit: role='lead', is_lead=0, lead_member_id NULL.

Best-effort like the rest of that block, since the membership and grant already
stand on their own.

* fix(agents): one definition of which scopes require a project binding

Qodo caught that the auth-request approval path still granted files_* and
project_tasks_create globally. The project-scope set existed as three parallel
copies (two function-local, one module-level), and the earlier fix only
corrected the module-level one -- leaving the path an invite actually takes
still writing those grants with project_id=None.

check_agent_scope_for_project only matches a grant bound to that exact project,
so a global grant never matches. The approval returns 200, the operator
believes access was granted, and the agent silently has none.

Now defined once and referenced everywhere; _SCOPE_* are plain aliases rather
than rebuilt literals, since re-listing the members is how the copies drifted.

Adds the regression test that was missing: nothing pinned this set, which is
why three copies could disagree unnoticed. Checks alias identity (not equality)
so a re-introduced copy fails even while it still happens to agree, asserts a
single assignment per name in the module source, and asserts every project-bound
scope is in VALID_SCOPES -- a typo there fails open, granting globally.

* ci: raise the test timeout above the actual suite runtime (#2134)

The cap was 45 min with a comment claiming 3.12/3.13 finish in ~16. Measured
over the last 12 job records that is no longer true: 3.13 takes 31-41 min and
3.12 takes 38-41, so the cap sat roughly 4 minutes above the slowest normal run.

Two of those 12 were killed mid-suite with nothing actually wrong, including
the one gating #2127. A cap that close to the median does not catch hangs, it
manufactures red PRs, and a timeout kill is indistinguishable from a real
failure until you check the clock against timeout-minutes. That is the worst
property a merge gate can have.

75 keeps a bound on a genuinely hung job while leaving real headroom. The suite
growing from ~16 to ~40 min is its own problem and is filed separately; this
stops it corrupting merge decisions in the meantime.

* feat(feedback): add per-user 24h submission cap (#2131)

* feat(feedback): add per-user 24h submission cap

* fix(feedback): make the 24h cap atomic (Qodo)

The cap did a SELECT COUNT then an INSERT as two awaited calls. Every
sequential test passes and the limit still does not hold: concurrent requests
all read the same count, all see room, and all insert. Verified against the
pre-fix code, 8 racers with one slot left all got 201 and took a cap of 20 to
27. A user with a few tabs open trips this without trying, and a scripted
client defeats it outright.

Moved enforcement into the store as a single INSERT ... SELECT ... WHERE
(SELECT COUNT(*) ...) < ?, so SQLite's write lock makes the check and the
insert indivisible, and rowcount says which way it went.

Adds the concurrency test that was missing, plus one asserting a rejected
submission leaves no row behind: a partially-written reject would tighten the
cap on every retry.

* refactor(feedback): drop count_recent, orphaned by the atomic cap

This PR added count_recent for the route to call before inserting. Moving the
cap into create_within_cap left it with no callers anywhere in the tree, so it
is dead on arrival rather than pre-existing code worth keeping.

Removing it also removes the tempting wrong path: a future caller reaching for
count_recent would reintroduce exactly the check-then-insert race the atomic
statement exists to close.

* test: replace always-true assert with issubclass check in test_installer_class_available (#1979)

* fix(install): route LXC port allocation through centralized allocator

Replace the standalone _find_free_port() in lxc_installer.py with
allocate_host_port(app_id) from port_allocator.py so the centralized
allocator is the single source of truth for all app host-port
assignments.

- Remove _find_free_port(), socket, and closing imports from lxc_installer
- Import allocate_host_port instead of RESERVED_PORTS
- Accumulate failed ports in exclude set across TOCTOU retry loop
- Fix stale _docker_published_port docstring (DockerInstaller now
  maps {allocated_host_port}:{container_port}, not {p}:{p})
- Add test class verifying _find_free_port is gone and
  allocate_host_port is the only import

Refs: #695

* test: strengthen allocator import assertion per Kilo suggestion

Add assert not hasattr(mod, 'RESERVED_PORTS') to verify the old import
is truly removed, not just that allocate_host_port is present.

* test: replace always-true assert with issubclass check in test_installer_class_available

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>

* feat(wallpaper): add Wallhaven proxy route + sectioned picker integration (#1902)

* feat(wallpaper): add Wallhaven proxy route + browse-online picker section

- Add wallhaven_api_key config field (env-only, never in repo)
- Create GET /api/wallhaven/search proxy route to wallhaven.cc API
- Keyless by default; optional X-API-Key header when WALLHAVEN_API_KEY set
- Handle rate limiting (429), timeouts (504), and Wallhaven errors (502)
- New WallhavenBrowser component: debounced search, thumbnail grid, pagination
- Integrate WallhavenBrowser into WallpaperPicker as collapsible section
- WallpaperPicker: "Browse online" toggle expands search UI, selecting a
  Wallhaven image applies it as a remote wallpaper
- Backend tests (test_wallhaven.py, 12 tests) with respx mocking
- Frontend tests (WallhavenBrowser.test.tsx, 8 tests; WallpaperPicker 14 tests)

Fixes #864

* fix(wallpaper): escape CSS url, persist wallpaperIdByTheme, guard JSONResponse, validate categories/purity, move os import

- Escape single-quotes and backslashes in remote wallpaper URLs to prevent
  CSS injection and render breakage (WallpaperPicker.tsx onSelect).
- Route through a state updater that sets wallpaperIdByTheme, and include
  light/mobile/fallback variants so theme switch preserves remote wallpapers.
- Use the received label as wallpaperOverlayText instead of discarding it.
- Guard resp.json() with try/except ValueError and cap response at 1 MB
  (wallhaven.py:71).
- Validate categories/purity with ^[01]{3}$ before forwarding to Wallhaven.
- Move import os as _os from inside load_config to module top (config.py).

* fix(wallpaper): harden CSS url() escaping — escape parens, strip control chars, validate http(s) scheme

Kilo WARNING: CSS url…
Pins the behavior a non-Gitea LXC manifest (service_name, ports,
ui_port, ui_path, state_paths) must get once the generic install path
lands: systemd unit named from service_name, no dl.gitea.com binary
download, no /etc/gitea/app.ini, declared ports published, state_paths
created in-container, and admin_password not required when no admin user
is declared.

Each test is xfail(strict=True, reason=...) so they stay green today by
failing as expected, and become XPASS failures the instant the
implementation ships -- forcing the marker to be removed.
Acceptance test: generic (non-Gitea) LXC service install, xfail-strict until implemented
- scripts/check_store_wiring.py detects PRs that add a new BaseStore
  subclass without wiring it into tinyagentos/app.py
- Uses name-level check (class name appears in app.py)
- Only flags newly added classes; pre-existing orphans are skipped
- Store-Unwired-Intentionally: <ClassName>, <why> trailer waives and logs
- .github/workflows/store-wiring-gate.yml runs the check on PRs
- tests/test_check_store_wiring.py proves FAIL, PASS, existing orphan
  not flagged, trailer waiver, and transitive subclass detection
…se, honest docs

- hold SAFE_POINT state across cancel window, queue arriving messages,
  remove destructive _message_queue.clear() in handle_message
- use asyncio.wait instead of asyncio.wait_for in await_subagent and
  await_all_subagents so a timeout never cancels the subagent task
- reuse _create_supervised_task for progress and runner tasks,
  cancel_and_wait for bounded cancellation, prune settled subagents and
  delivered entries, switch UI timestamps to time.time()
- update changelog to describe library as not-yet-wired infrastructure,
  add not-yet-integrated banner to design doc
Fix-forward #2335: safe-point race + wait_for cancellation + task_utils reuse + wiring design note
Add the design law to 01-rules.md with a worked example anonymised as
'an agent', link it from index.md, and rebuild the compiled manual. Trim
verbose prose in the image-prompting guide to stay within the compiled
manual 18000-char budget. Update CHANGELOG.
Agent guides: add the mechanical-simple-auditable design law
…faces

/auth/status and /auth/me validated sessions without the User-Agent the
API middleware checks, so a session whose UA hash stopped matching (a
browser auto-update rotates the UA string) read authenticated on status
while every /api/* call returned 401. The SPA's LoginGate treats that
contradiction as session-expired, re-checks status, gets authenticated,
remounts the shell, and loops - the PWA refresh loop observed on the
beta.46 deployment. The chat, canvas, terminal and web-chat WebSocket
handlers had the inverse hole: they accepted a cookie the APIs reject.

All six call sites now pass the request's User-Agent, so the stolen-
cookie binding check gives one answer everywhere. Sessions created
without a UA hash continue to validate regardless, unchanged.
fix(auth): apply session User-Agent binding uniformly across auth surfaces
…nstall

The five Hailo-10H HEF manifests declared requires.backends without a
targets list, so hardware_to_targets() never emitted a matching target
and resolve() rejected every device, including a real Pi 5 + Hailo-10H.

- hardware_to_targets(): add a hailo10h NPU branch emitting "hailo",
  mirroring the existing rockchip branch.
- Each HEF manifest's hailo-ollama backend entry now declares
  targets: [hailo], matching the rkllm manifests' targets: [rockchip]
  convention.
- Add tests/catalog/test_resolver_hailo.py: loads a real manifest from
  app-catalog and resolves it against a simulated Pi 5 + Hailo-10H
  profile (expects ResolveOk/hailo-ollama) and a CPU-only x86 profile
  (expects ResolveErr).
…pages

sha256 was empty on all five HEF manifests, which silently skips
integrity verification on download. Pulled the pinned per-model
hef_h10h digests from hailo-ai/hailo_model_zoo_genai at tag v5.1.1
(the same release the download URLs point at) and spot-verified one
(Qwen2-1.5B-Instruct.hef) by streaming the file and comparing
sha256sum — exact match.

size_mb was also stale on every entry; recomputed from the actual
Content-Length of each dev-public.hailo.ai URL (bytes / 1048576,
rounded), which corrected two entries significantly:
qwen2.5-1.5b-instruct-hef 1679 -> 2250 and
deepseek-r1-distill-qwen-1.5b-hef 2427 -> 2261. Updated the two
matching "GB" labels for consistency. The other three shifted by only
a few MB.

llama3.2-3b-instruct-hef and qwen2.5-coder-1.5b-instruct-hef pointed
their homepage at the base (non-Instruct) HuggingFace model card;
both HEF variants are instruction-tuned, so point at the
*-Instruct card instead.
…ntion

Every other Llama 3.2 entry in the catalog uses a dashed
llama-3.2-<size> id (llama-3.2-1b, llama-3.2-3b). The new HEF manifest
dropped the dash before the version, so rename the directory and the
manifest's id field to match: llama3.2-3b-instruct-hef ->
llama-3.2-3b-instruct-hef.
CI check: a new BaseStore must be wired into the app lifespan (no orphan stores)
_MODEL_FILE_SUFFIXES drove both the downloaded-models listing and the
orphan-file scan in routes/models.py, but didn't know about .hef —
so files pulled by the new Hailo-10H HEF manifests would silently
never show up as local files or be considered for orphan cleanup.
…ine route

#2333 added StrikeStore and ProjectTaskStore's strikes= param but never
constructed the store or passed it in, so quarantine/close-time strike
clearing was dead code and the strikes= param was inert.

- Construct StrikeStore in create_app(), init/close it in the lifespan
  alongside the other project-scoped stores, attach it as
  app.state.task_strikes, and pass it into ProjectTaskStore(strikes=...).
- Fix tinyagentos/projects/ids.py: StrikeStore.record_strike calls
  new_id("str") but "str" was never registered in ID_PREFIXES, so every
  strike record raised ValueError. Without this the feature never worked
  at all, wiring or not.
- Surface strike_count + latest_strike on GET task detail.
- Add a LEAD-only POST .../tasks/{id}/unquarantine route (mirrors the
  claimable curation gate), wired into the agent-token allowlist and
  docs/agent-coordination.md.
- tests/conftest.py's client fixture bypasses the lifespan and manually
  inits every store it touches; task_strikes needed the same treatment
  or GET task detail 500s under that fixture.
- Add tests/projects/test_strike_wiring.py exercising the real app
  lifespan (app.router.lifespan_context) rather than the bypass fixture,
  since the bypass would pass even with the wiring missing.
README's catalog counts were stale at 113 in four places (dev had
already moved to 115 before this PR, and this PR's five HEF manifests
bring it to 120 — verified by counting app-catalog/models/*/manifest.yaml
directories in-tree). Also "verified against HuggingFace" is no longer
true now that the Hailo HEF manifests point at dev-public.hailo.ai, so
reword to "its upstream host", and call out the new Hailo-10H HEF
variants alongside the existing RK3588 NPU mention.
routes/models.py now recognizes .hef files, which is a user-visible
behaviour change and trips the doc-gate's user-visible-changelog rule
(on_modify on tinyagentos/routes/*.py). Add the changelog.d fragment
per docs/changelog-fragments.md instead of editing CHANGELOG.md
directly.
AgentChatRouter drives OpenClaw ACP turns through one AgentLoop per agent
(replacing the per-agent asyncio.Lock): the turn-holder drives its turn,
then iteratively drives every message queued at the safe point, each with
its own trace id. reach_safe_point runs in a finally so a raising turn can
never wedge the loop in WORKING.

The desktop taOS agent chat endpoint serializes on app.state.taos_agent_loop,
fixing a race where two concurrent POSTs shared the opencode session with no
serialization. A concurrent request gets a queued-notice NDJSON frame; the
turn-holder surfaces queued message contents into its stream tail before the
final done frame (redrive out of scope).

New GET /api/taos-agent/status returns the desktop loop's status scoped to
state / current_turn_id / queued_count / subagents [{id, task, state,
started_at}] with subagent result/error stripped.

Refs: tsk-icpt4i
AppManifest declared no context_window field and from_dict never read the
YAML value, so every catalog manifest loaded as 0. All consumers read it
via getattr(manifest, "context_window", 0), so the chat context-window
budget code (routes/store.py, routes/store_install.py, routes/taosmd.py,
agent_chat_router.py, routes/agents.py, chat/reactions.py) always hit the
4000-token "unknown window" fallback.

Add context_window: int = 0 to AppManifest (0 == unknown, preserving the
existing getattr fallback semantics) and wire it into from_dict. Real windows
now flow into history_token_budget, which floors tiny windows (e.g. rkllm
4096 -> 512) instead of defaulting to 4000.

Tests (red-first): a manifest declaring context_window=4096 loads onto
AppManifest; a manifest without it defaults to 0. Plus a build_context_window
budget test for a known small 4096-token window, asserting oldest-first
trimming under the resulting 512-token budget.

#2338, #1740
Make 3-strike QUARANTINE loud: store + lifespan wiring + surfacing + tests (FULL slice, replaces tsk-glxi4e)
store-wiring gate v2: AST-level wiring check + rename-status files + waiver cleanup
context_window is inert repo-wide: AppManifest has no such field, every manifest value is silently dropped
jaylfc added 8 commits August 11, 2026 20:12
…k-u23vjy)

Re-scoped fix-forward of closed duplicate PR #2183. Of the card's four
defects, two are already resolved or impossible on dev: position-0
collision was fixed by #2265's atomic in-INSERT allocation (existing
concurrency test guards it), and NULL list_id rows cannot exist (schema
NOT NULL). The two real ones land here:

- get_entry read cur.description outside the cursor context; moved inside.
- reorder_entries left already-issued UPDATEs pending when one raised, so
  the next unrelated commit() flushed a half-applied reorder; now rolls
  back and re-raises, with a test proving the pending write neither
  survives immediately nor resurfaces via a later unrelated commit
  (proven red against the unguarded store).
…it()

CodeRabbit's Major on #2361, folded: asyncio.CancelledError does not
inherit Exception, so task cancellation mid-reorder left the issued
UPDATEs pending exactly like the original hazard, and commit() sat
outside the guard. The commit moves inside the try and the handler
catches BaseException, rolling back before re-raising. Regression tests
for both paths; the cancellation test proven red against the
except-Exception guard.
fix(lists): get_entry cursor scope + reorder rollback on failure (tsk-u23vjy)
…(tsk-hfs6zv)

routes/agent_model_api.py enforces its own consent-key auth (never
resolves a model without a valid key, OpenAI-shaped 401 otherwise) but
the middleware had no passthrough for /v1 paths, so every external
OpenAI-compatible caller was rejected by the session gate before the
handler ran - the surface was dead code from outside (found by hermes's
live probe, bus 2380).

Exempts exactly GET /v1/models and POST /v1/chat/completions,
method-sensitive; /v1/anything-else and wrong-method requests stay
session-gated, with tests for both directions. External-caller tests
(no session cookie) proven red against the unmodified middleware,
including a minted-key 200 end to end. The chat route's 501-until-seam
behavior is untouched and now documented in agent-coordination.md.
fix(auth): consent-key passthrough for the /v1 agent-model surface (tsk-hfs6zv)
…r has NO protection (#2362)

* chore(security): gate .gitignore secret ignores on master/dev/release/*

Add scripts/check_secret_ignores.py, which asserts that the committed
.gitignore (a) still contains every required secret-protection rule as an
active line and (b) ignores a canonical set of secret-shaped paths
(data/hub/identity.json, foo.key, creds.json, x.p8, y_credentials.json, ...)
via git check-ignore. Covered by tests/test_check_secret_ignores.py,
including a parametrized test that drops each required pattern from a copy of
the real .gitignore and proves the guard goes red, plus a real-tree
regression asserting this branch is green.

.github/workflows/secret-ignores-gate.yml runs the guard on push and PR to
master, dev, and release/*, so a dropped pattern fails the branch it lands on
rather than being assumed during dev->master promotion.

Docs: Secret-ignores gate section in the contributor skill and a
post-promotion verification step in docs/RELEASING.md. Changelog fragment
added.

Refs tsk-laezfg.

* fix(security): ignore data/.litellm_master_key and add it to the gate canon

The LiteLLM proxy master key has a bare _key suffix, so neither *.key nor
data/*.key matches it: the file has been live, untracked, and one
git add -A away from staging on dev boxes (long-standing queue item).
This PR is the reviewed route for exactly that class of rule, so the rule
lands here with both gate signals covering it; removal proven red:

    SECRET-IGNORE FAIL: .gitignore is missing required protection patterns:
      - data/.litellm_master_key

* docs(releasing): tag the check_secret_ignores fence as bash (MD040)
Collate 15 changelog.d fragments into the 1.0.0-beta.48 section (merged with
the direct [Unreleased] entries under single headings), bump the four version
files including uv.lock's normalized 1.0.0b48.

Docs-Reviewed: release version bump; no CI, packaging or contribution rule change
chore(release): v1.0.0-beta.48 version bump + changelog
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@github-actions

Copy link
Copy Markdown

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

See CONTRIBUTING.md for the branch model.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Beta 48 adds task quarantine controls, AgentLoop-based chat coordination, Hailo model manifests, wallpaper-fit preferences, registry handle editing, authorization changes, chat export support, CI validation gates, tests, documentation, and release metadata.

Changes

Validation gates and release controls

Layer / File(s) Summary
Store and secret validation gates
.github/workflows/*, scripts/check_*, tests/test_check_*
Added Git-aware checks for new store wiring and secret-ignore protections.
CI runtime and release metadata
.github/workflows/ci.yml, CHANGELOG.md, pyproject.toml, tinyagentos/__init__.py, desktop/package.json
Updated Node.js to 22 and versioned the release as beta.48.

Hailo model catalog

Layer / File(s) Summary
Hailo manifests and resolver support
app-catalog/models/*hef, tinyagentos/cluster/capabilities.py, tinyagentos/routes/models.py, tests/catalog/*
Added five Hailo HEF manifests and Hailo hardware resolution coverage.
Manifest metadata and integrity validation
tinyagentos/registry.py, tests/test_model_manifest_integrity.py, tests/test_registry.py
Loaded context windows and validated manifest targets, hashes, URLs, and sizes.

Desktop presentation controls

Layer / File(s) Summary
Wallpaper-fit persistence and rendering
desktop/src/stores/theme-store.ts, desktop/src/components/*, desktop/src/apps/SettingsApp.tsx, desktop/src/theme/tokens.css, desktop/src/stores/__tests__/*
Added per-device wallpaper-fit persistence, settings controls, CSS modes, and tests.
Registry handle editing and row behavior
desktop/src/apps/agents/RegistryPanel.*
Added authorized handle editing and status-section behavior tests.
GuidesApp coverage
desktop/src/apps/GuidesApp.test.tsx
Added tests for loading, recommendations, errors, and empty results.

AgentLoop coordination

Layer / File(s) Summary
AgentLoop lifecycle and subagents
tinyagentos/agent_loop.py, docs/design/agent-loop-subagents.md, tests/test_agent_loop.py
Added loop states, queued messages, safe points, supervised subagents, cancellation, status, and wait operations.
ACP and desktop chat integration
tinyagentos/agent_chat_router.py, tinyagentos/routes/taos_agent.py, tests/test_agent_chat_router.py, tests/test_taos_agent_chat.py
Serialized per-agent turns, queued concurrent messages, drained queued frames, and exposed loop status.

Task quarantine and persistence

Layer / File(s) Summary
Strike storage and task lifecycle
tinyagentos/projects/strike_store.py, tinyagentos/projects/task_store.py, tinyagentos/app.py, tinyagentos/routes/projects.py, tests/projects/test_strike_wiring.py, tests/test_task_store.py
Added strike persistence, quarantine transitions, unquarantine routing, cleanup, auditing, and integration tests.
Project list transaction handling
tinyagentos/projects/lists_store.py, tests/projects/test_lists_store.py
Fixed cursor access and rollback behavior for reorder failures.

Authentication and authorization boundaries

Layer / File(s) Summary
Middleware and session validation
tinyagentos/auth_middleware.py, tinyagentos/routes/auth.py, tinyagentos/routes/{canvas,channel_hub,chat,terminal}.py, tests/test_auth.py, tests/test_routes_agent_model_api.py
Added exact model-route exemptions and consistent User-Agent session validation.
Existence-hiding authorization
tinyagentos/routes/agent_*.py, tests/test_agent_registry.py, tests/test_agent_scope_requests.py, related route tests
Unauthorized registry and scope-request targets now return indistinguishable 404 responses.
Path and invite validation
tinyagentos/routes/memory.py, tests/test_routes_memory.py, tests/test_routes_project_invites.py
Rejected unsafe memory paths and added invite scope and rollback coverage.

Chat export and route coverage

Layer / File(s) Summary
A2A chat export
tinyagentos/chat/chat_exporter.py, tests/test_chat_exporter.py
Added deterministic multi-channel export with content handling, reply filtering, and size limits.
Route and context regression coverage
tests/test_chat_context_window.py, tests/test_routes_mcp.py, tests/conftest.py
Added context-window, MCP endpoint, and fixture coverage.

Documentation and installation coverage

Layer / File(s) Summary
Agent and Radio Studio designs
docs/design/*, docs/agent-manual/*, docs/taos-agent-manual.md, docs/mirror-policy.md
Added AgentLoop and Radio Studio specifications and revised agent guidance.
Generic LXC installation checks
tests/test_lxc_installer.py
Added generic installation fixtures and strict acceptance tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • jaylfc/taOS#2333: Adds the same StrikeStore, task lifecycle wiring, unquarantine route, and tests.
  • jaylfc/taOS#2335: Adds the same AgentLoop implementation, design documentation, and tests.
  • jaylfc/taOS#2334: Adds the same store-wiring gate, checker, workflow, documentation, and tests.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the release promotion from dev to master and names version v1.0.0-beta.48.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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

❤️ Share

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

@gitar-bot

gitar-bot Bot commented Aug 11, 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

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

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_agent_scope_requests.py (1)

440-462: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the stale expected-status comment.

The comment says authorization returns 403, but Line 462 correctly asserts 404. State that authorization runs before scope validation and returns 404.

Proposed fix
-        # Authz runs first -> 403, NOT a 400 vocab error confirming the bad scope.
+        # Authz runs first -> 404, NOT a 400 vocab error confirming the bad scope.
🤖 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 `@tests/test_agent_scope_requests.py` around lines 440 - 462, Update the
assertion comment in test_create_authorizes_before_scope_vocab to state that
authorization runs before scope validation and returns 404, matching the
existing resp.status_code assertion.
🟡 Minor comments (15)
tests/test_lxc_installer.py-791-797 (1)

791-797: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert a state-path creation operation.

The assertion passes when any command only references /var/lib/taosr1. A generic installer can then XPASS without creating the directory. Assert that a directory-creation operation targets the declared path.

Proposed test change
-        assert any(
-            "/var/lib/taosr1" in cmd for cmd in full_cmds
-        ), "state_paths must be created inside the container (/var/lib/taosr1)"
+        assert any(
+            "/var/lib/taosr1" in cmd and ("mkdir" in cmd or "install -d" in cmd)
+            for cmd in full_cmds
+        ), "state_paths must be created inside the container (/var/lib/taosr1)"
🤖 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 `@tests/test_lxc_installer.py` around lines 791 - 797, Update
test_state_paths_created_in_container to assert that captured commands include a
directory-creation operation targeting /var/lib/taosr1, rather than merely
checking for the path text. Match the installer’s actual mkdir command structure
while preserving the requirement that creation occurs inside the container.
tests/test_routes_mcp.py-44-44 (1)

44-44: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unused app bindings.

Ruff reports RUF059 on Lines 44 and 92. Bind the unused tuple value to _.

Proposed fix
-        client, app = app_client
+        client, _ = app_client

Also applies to: 92-92

🤖 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 `@tests/test_routes_mcp.py` at line 44, Update the tuple unpacking in the
affected test setup statements to bind the unused second value to `_` instead of
`app`, including both occurrences around the `app_client` assignments. Keep the
`client` binding unchanged.

Source: Linters/SAST tools

tests/test_chat_context_window.py-47-55 (1)

47-55: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use distinct message content to verify suffix retention.

All messages have the same content on Line 48. The assertion on Line 55 passes for any retained subset and does not verify that build_context_window kept the newest contiguous suffix.

Proposed test correction
-    message = "x" * 400  # 100 tokens per message
-    msgs = [_msg("user", message) for _ in range(20)]  # 2000 tokens total
+    msgs = [_msg("user", f"{index:04d}" + "x" * 396) for index in range(20)]
     ctx = build_context_window(msgs, limit=20, max_tokens=budget)
 ...
-    assert [m["content"] for m in ctx] == [message] * len(ctx)
+    assert [m["content"] for m in ctx] == [
+        m["content"] for m in msgs[-len(ctx):]
+    ]
🤖 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 `@tests/test_chat_context_window.py` around lines 47 - 55, Update the test
around build_context_window to assign distinct content to each message, such as
sequence-labeled values, while preserving the token-budget setup. Change the
final assertion to compare the retained messages against the expected newest
contiguous suffix, ensuring older messages are dropped first rather than merely
checking identical content.
tests/test_model_manifest_integrity.py-201-205 (1)

201-205: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Parse download_url before accepting it.

startswith("https://") accepts malformed values such as https://. It also accepts URLs without a hostname. Require a string URL with scheme == "https" and a non-empty netloc.

Proposed fix
+from urllib.parse import urlparse
+
 ...
-            if not url or not url.startswith("https://"):
+            parsed = urlparse(url) if isinstance(url, str) else None
+            if parsed is None or parsed.scheme != "https" or not parsed.netloc:
                 errors.append(
                     f"{mid}/{vid}: download_url must be a non-empty https URL (got {url!r})"
                 )
🤖 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 `@tests/test_model_manifest_integrity.py` around lines 201 - 205, Update the
download_url validation in the model manifest integrity check to parse the value
as a URL before accepting it. Require a string value whose parsed scheme is
exactly "https" and whose netloc is non-empty, while preserving the existing
error reporting through errors.append for invalid URLs.
tests/test_auth.py-1256-1259 (1)

1256-1259: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the expected successful response.

Line 1259 accepts a 404 or 500 response. The matching User-Agent case must return 200.

Proposed fix
-        assert ok.status_code != 401
+        assert ok.status_code == 200
🤖 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 `@tests/test_auth.py` around lines 1256 - 1259, Update the assertion in the
auth client test around the /auth/me request to require status code 200 for the
matching old-browser User-Agent, rather than merely accepting any non-401
response.
scripts/check_store_wiring.py-218-221 (1)

218-221: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exclude BaseStore from the subclass result.

_inherits_base_store("BaseStore", ...) returns True. Line 218 therefore reports a newly added BaseStore as an unwired subclass. The gate must only inspect subclasses.

Proposed fix
     return {
         name for name in classes_in_file
-        if _inherits_base_store(name, all_classes)
+        if name != "BaseStore" and _inherits_base_store(name, all_classes)
     }

Add a regression test for a file that defines only BaseStore.

🤖 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 `@scripts/check_store_wiring.py` around lines 218 - 221, Exclude the
`BaseStore` symbol from the subclass set returned by the surrounding
store-wiring discovery function, while preserving detection of all other classes
that inherit from it. Add a regression test covering a file defining only
`BaseStore` and assert that no unwired subclass is reported.
.github/workflows/secret-ignores-gate.yml-30-30 (1)

30-30: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Disable persisted checkout credentials in both gate workflows.

Both workflows run repository-controlled scripts after checkout. Set persist-credentials: false in both actions/checkout@v7 steps. The store-wiring workflow can still fetch the public origin without the token.

🤖 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 @.github/workflows/secret-ignores-gate.yml at line 30, Disable persisted
checkout credentials by setting persist-credentials to false on the
actions/checkout@v7 step in .github/workflows/secret-ignores-gate.yml at lines
30-30 and .github/workflows/store-wiring-gate.yml at lines 32-34; apply the same
change to both workflow checkout steps.

Source: Linters/SAST tools

desktop/src/apps/agents/RegistryPanel.test.tsx-800-803 (1)

800-803: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update RegistryStatus to accept unknown server statuses.

RegistryStatus is a closed union, so the "frozen" fixture does not type-check and the test cannot compile.

🤖 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 `@desktop/src/apps/agents/RegistryPanel.test.tsx` around lines 800 - 803,
Update the RegistryStatus type used by RegistryEntry and RegistryStatus handling
to accept arbitrary server-provided status strings, while preserving existing
recognized-status behavior. Ensure the "frozen" fixture in the
unrecognised-status test type-checks and follows the existing Other-section
rendering path.
docs/agent-manual/01-rules.md-26-26 (1)

26-26: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use afterward in both manual files.

The same American-English locale violation appears in the source and generated manual. Fix the source, then regenerate the generated output.

  • docs/agent-manual/01-rules.md#L26-L26: replace afterwards with afterward.
  • docs/taos-agent-manual.md#L41-L41: regenerate this file from the corrected source.
🤖 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 `@docs/agent-manual/01-rules.md` at line 26, Replace “afterwards” with
“afterward” in docs/agent-manual/01-rules.md:26, then regenerate
docs/taos-agent-manual.md so docs/taos-agent-manual.md:41 reflects the corrected
source; do not edit the generated file independently.

Source: Linters/SAST tools

docs/design/radio-studio.md-100-100 (1)

100-100: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to all fenced blocks.

Static analysis flags these seven fences. Use bash for command examples and text for diagrams, routes, and file maps.

Also applies to: 106-106, 265-265, 281-281, 334-334, 423-423, 451-451

🤖 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 `@docs/design/radio-studio.md` at line 100, Add language identifiers to all
seven flagged fenced code blocks in the document: use bash for command examples
and text for diagrams, routes, and file maps.

Source: Linters/SAST tools

docs/taos-agent-manual.md-45-45 (1)

45-45: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the generated section separators.

The new prose is immediately followed by ---. Markdown parses the preceding prose as a setext heading, which triggers MD003. Add a blank line before the separator in scripts/build-agent-manual.py or emit an explicit thematic break, then regenerate this file.

Also applies to: 265-267

🤖 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 `@docs/taos-agent-manual.md` at line 45, Update the generated-section
formatting in scripts/build-agent-manual.py so newly emitted prose is separated
from the following `---` by a blank line, or emit an explicit thematic break
instead. Regenerate docs/taos-agent-manual.md and ensure the affected separators
no longer parse the preceding prose as setext headings.

Source: Linters/SAST tools

tinyagentos/projects/strike_store.py-73-87 (1)

73-87: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make strike ordering deterministic.

created_at can be equal for consecutive inserts. In that case, SQLite can return either row as the latest strike. This can make the response and tests/projects/test_strike_wiring.py flaky.

Add an insertion-order tie breaker to both queries.

Proposed fix
- "SELECT * FROM task_strikes WHERE task_id = ? ORDER BY created_at ASC",
+ "SELECT * FROM task_strikes WHERE task_id = ? ORDER BY created_at ASC, rowid ASC",

- "SELECT * FROM task_strikes WHERE task_id = ? ORDER BY created_at DESC LIMIT 1",
+ "SELECT * FROM task_strikes WHERE task_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1",
🤖 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/projects/strike_store.py` around lines 73 - 87, The list and
latest queries in the strike store must use deterministic ordering when
created_at values tie. Update both ORDER BY clauses in the methods that fetch
strikes and latest to add the insertion-order column as a secondary ascending or
descending tie breaker matching each query’s direction.
tests/test_chat_exporter.py-649-670 (1)

649-670: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Extend this test with characters that JSON escaping expands.

The body is only "x" repeated. JSON serialization does not expand it, so this test cannot detect the size-budget gap in tinyagentos/chat/chat_exporter.py. Add a case whose body consists of quotes, backslashes, or control characters. The serialized envelope then grows well beyond the raw byte count.

🤖 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 `@tests/test_chat_exporter.py` around lines 649 - 670, Extend
test_oversize_envelope_never_exceeds_limit_serialized with JSON-escaping-heavy
content, such as quotes, backslashes, or control characters, instead of relying
only on repeated "x" characters. Keep the existing serialized-envelope length
assertion and ensure the payload exercises expansion between raw content size
and json.dumps output.
tinyagentos/chat/chat_exporter.py-38-56 (1)

38-56: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle blocks that are neither dict nor str.

A block of any other type (for example a number or null in the stored JSON) contributes nothing. The body can then become empty while content_blocks is non-empty. Line 188 then raises ChatExportError and aborts the whole channel export. This contradicts the docstring claim that a non-text block never disappears.

🛠️ Proposed fallback placeholder
         elif isinstance(block, str):
             if block:
                 parts.append(block)
+        elif block is not None:
+            parts.append(f"[{type(block).__name__} block]")
     return "\n".join(parts)
🤖 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/chat/chat_exporter.py` around lines 38 - 56, Update the
block-processing function around the existing dict/string branches to handle
every other block type by appending a non-empty fallback placeholder, rather
than silently skipping it. Preserve current formatting for dict and string
blocks, ensuring non-text entries such as numbers or null remain represented so
content_blocks cannot produce an empty body solely because unsupported types
were discarded.
tinyagentos/chat/chat_exporter.py-146-152 (1)

146-152: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Handle unknown content_blocks values without aborting export. flatten_body skips non-dict, non-string elements such as 123. A list like [123] produces an empty body, so line 188 raises ChatExportError and aborts the channel export. Add a fallback placeholder for unsupported block values.

🤖 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/chat/chat_exporter.py` around lines 146 - 152, Update the export
flow around flatten_body to handle unsupported content_blocks values without
raising ChatExportError. Ensure flatten_body emits a fallback placeholder for
non-dict, non-string blocks such as numeric values, so channels with content
like [123] continue exporting instead of producing an empty body.
🧹 Nitpick comments (9)
tinyagentos/routes/taos_agent.py (1)

585-589: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer an explicit GeneratorExit handler over sys.exc_info().

sys.exc_info()[1] inside finally works only while the exception propagates. An explicit handler states the intent and removes the sys import dependency for this check.

♻️ Proposed alternative
         except Exception as exc:
             logger.exception("taos-agent: generator error")
             yield json.dumps({"error": str(exc)}) + "\n"
             content_frame_yielded = True
+        except GeneratorExit:
+            client_gone = True
+            raise

Set client_gone = False next to leftovers, then log on if leftovers and client_gone.

🤖 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/routes/taos_agent.py` around lines 585 - 589, Update the
surrounding generator flow to catch GeneratorExit explicitly and set a
client-disconnected flag, initialized alongside leftovers; replace the
sys.exc_info() check in the warning condition with that flag, and remove the
now-unused sys import.
tests/test_taos_agent_chat.py (1)

332-339: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the assertion out of the fake adapter.

prompt runs inside the supervised _drive task. _drive catches Exception, so a failing assert action is LoopAction.QUEUED becomes an error frame in the stream instead of a test failure. Record the action on a nonlocal and assert it after the response.

🤖 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 `@tests/test_taos_agent_chat.py` around lines 332 - 339, Update the fake
adapter’s prompt method to store the result of loop.handle_message in a nonlocal
test variable instead of asserting there; after the supervised response/stream
completes, assert that recorded action is LoopAction.QUEUED from the outer test
context so assertion failures propagate to the test.
tinyagentos/agent_loop.py (2)

182-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_prune_subagents discards every completed handle once the cap is passed.

When len(self._subagents) exceeds _max_subagents, the method deletes all done entries, not just the oldest ones. A caller that later calls await_subagent on one of those ids gets KeyError, and get_subagent returns None. Consider removing only the oldest done entries until the map is back at the cap.

♻️ Proposed change
     def _prune_subagents(self) -> None:
         if len(self._subagents) <= self._max_subagents:
             return
-        to_remove = [
+        done_ids = [
             sid for sid, entry in self._subagents.items()
             if entry.task_obj is not None and entry.task_obj.done()
         ]
-        for sid in to_remove:
+        excess = len(self._subagents) - self._max_subagents
+        for sid in done_ids[:excess]:
             del self._subagents[sid]
🤖 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/agent_loop.py` around lines 182 - 190, Update _prune_subagents to
remove only the oldest completed entries, stopping once len(self._subagents) is
at or below _max_subagents. Preserve completed handles until pruning requires
their removal, and retain newer done entries so await_subagent and get_subagent
can still access them.

432-443: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse the duplicated result branches.

Both branches return entry.handle.result. The state == "cancelled" check has no effect in either location.

♻️ Proposed simplification
         if entry.task_obj.done():
-            if entry.handle.state == "cancelled":
-                return entry.handle.result
             return entry.handle.result
-        done, pending = await asyncio.wait([entry.task_obj], timeout=timeout)
+        _done, pending = await asyncio.wait([entry.task_obj], timeout=timeout)
         if entry.task_obj in pending:
             raise asyncio.TimeoutError(
                 f"subagent {sub_id} did not finish within {timeout}s"
             )
-        if entry.handle.state == "cancelled":
-            return entry.handle.result
         return entry.handle.result

The _done rename also clears the Ruff RUF059 hint at Line 436. Line 481 has the same hint.

🤖 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/agent_loop.py` around lines 432 - 443, In the result-waiting
logic around the relevant agent-loop method, remove the redundant
cancelled-state condition and return entry.handle.result through a single path
for both already-completed and newly-completed tasks. Rename the unused done
value from asyncio.wait to _done, and apply the same unused-result rename at the
corresponding wait site around line 481.

Source: Linters/SAST tools

docs/design/agent-loop-subagents.md (1)

64-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the language tag onto the fence.

The fence opens without a language and the first content line is the literal word text. That word renders inside the diagram. markdownlint reports MD040 for the same fence.

📝 Proposed fix
-```
-text
+```text
 IDLE
🤖 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 `@docs/design/agent-loop-subagents.md` around lines 64 - 66, Update the diagram
code fence containing the IDLE state so the language tag is attached to the
opening fence as text, and remove the literal text line from the diagram
content. Keep IDLE as the first diagram line and ensure the fence is properly
closed.

Source: Linters/SAST tools

tests/test_agent_loop.py (1)

379-418: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider covering the retention caps.

_max_subagents and _max_delivered control pruning in tinyagentos/agent_loop.py. No test exercises either path. A small test that spawns past _max_subagents would pin the retention behavior, including which completed handles remain reachable through get_subagent.

🤖 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 `@tests/test_agent_loop.py` around lines 379 - 418, Add tests covering the
retention limits configured by AgentLoop._max_subagents and _max_delivered.
Exercise spawning beyond _max_subagents and verify pruning plus which completed
handles remain accessible through get_subagent; also cover delivered-message
pruning beyond _max_delivered, preserving existing status behavior.
tests/test_agent_chat_router.py (1)

820-841: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a cancellation case next to the failure case.

The suite covers a raising drive_turn. It does not cover cancellation of the _run_acp_turn task, which is the path AgentChatRouter.close() takes at shutdown. Add a test that cancels the task mid-turn and then asserts the loop state, so the drain invariant in tinyagentos/agent_chat_router.py Lines 89-93 is pinned by a test.

🤖 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 `@tests/test_agent_chat_router.py` around lines 820 - 841, Add an async
cancellation test adjacent to
test_run_acp_turn_drive_failure_does_not_wedge_loop, using a blocking fake
drive_turn and cancelling the _run_acp_turn task while it is mid-turn. Await the
cancellation, then assert the agent loop state is IDLE, covering the shutdown
path used by AgentChatRouter.close() and the drain invariant in _run_acp_turn.
tests/projects/test_lists_store.py (1)

400-437: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

The test simulates a raised CancelledError, not real task cancellation.

cancelling_execute raises asyncio.CancelledError() directly. The enclosing task is never cancelled, so await self._db.rollback() in the production handler completes normally.

Under real cancellation the behavior differs. If asyncio.Task.cancel() cancels the task, the await inside the except BaseException handler can raise CancelledError again before rollback() finishes. The rollback is then lost, which is the exact failure the guard prevents.

Consider covering real cancellation and shielding the rollback.

♻️ Proposed additional test for real task cancellation
`@pytest.mark.asyncio`
async def test_reorder_entries_rolls_back_on_task_cancel(entries_store, monkeypatch):
    a = await entries_store.add_entry(
        list_id="lst-1", project_id="prj-1", text="A", original_text="A",
        author_kind="agent", author_id="agent-1",
    )
    b = await entries_store.add_entry(
        list_id="lst-1", project_id="prj-1", text="B", original_text="B",
        author_kind="agent", author_id="agent-1",
    )

    real_execute = entries_store._db.execute
    started = asyncio.Event()
    update_calls = 0

    async def slow_execute(sql, params=()):
        nonlocal update_calls
        if sql.startswith("UPDATE project_list_entries SET position"):
            update_calls += 1
            if update_calls == 2:
                started.set()
                await asyncio.sleep(3600)
        return await real_execute(sql, params)

    monkeypatch.setattr(entries_store._db, "execute", slow_execute)

    task = asyncio.create_task(
        entries_store.reorder_entries(
            "prj-1", "lst-1",
            [{"id": a["id"], "position": 1}, {"id": b["id"], "position": 0}],
        )
    )
    await started.wait()
    task.cancel()
    with pytest.raises(asyncio.CancelledError):
        await task
    monkeypatch.undo()

    await entries_store.add_entry(
        list_id="lst-2", project_id="prj-1", text="Unrelated",
        original_text="Unrelated", author_kind="agent", author_id="agent-1",
    )
    assert (await entries_store.get_entry(a["id"]))["position"] == 0
    assert (await entries_store.get_entry(b["id"]))["position"] == 1
🛡️ Proposed production change in tinyagentos/projects/lists_store.py to survive real cancellation
         except BaseException:
             # Without this, the UPDATEs already issued stay pending on the
             # shared connection and the next unrelated commit() flushes a
             # half-applied reorder. BaseException, not Exception: task
             # cancellation (CancelledError) must also roll back, and commit()
             # itself is inside the guard for the same reason.
-            await self._db.rollback()
+            # shield(): under real task cancellation a bare await in the
+            # handler can be cancelled again before the rollback completes.
+            await asyncio.shield(self._db.rollback())
             raise

This requires import asyncio in tinyagentos/projects/lists_store.py.

🤖 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 `@tests/projects/test_lists_store.py` around lines 400 - 437, Update
test_reorder_entries_rolls_back_on_cancellation to exercise real task
cancellation: run reorder_entries in an asyncio task, pause the second position
update with an event, cancel the task, and assert CancelledError while verifying
the entries were rolled back. Also update the production rollback handler in
reorder_entries to shield the rollback await from cancellation, adding the
required asyncio import.
tests/test_chat_exporter.py (1)

235-239: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Target the updated row by its id.

The UPDATE selects the row with ORDER BY created_at DESC LIMIT 1. It works only because ts is captured before the inserts and is therefore older than every new row. A later refactor that moves the ts assignment can make this statement rewrite the same row three times, and the test would still pass while covering nothing. send_message returns the message, so use its id.

♻️ Proposed test refactor
-    for i in range(3):
-        await store.send_message(
+    for i in range(3):
+        msg = await store.send_message(
             channel_id="ch1",
             author_id="user1",
             author_type="user",
             content=f"msg{i}",
             content_blocks=[{"type": "paragraph", "text": f"msg{i}"}],
         )
         await store._db.execute(
-            "UPDATE chat_messages SET created_at = ? WHERE id = (SELECT id FROM chat_messages ORDER BY created_at DESC LIMIT 1)",
-            (ts,),
+            "UPDATE chat_messages SET created_at = ? WHERE id = ?",
+            (ts, msg["id"]),
         )
         await store._db.commit()
🤖 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 `@tests/test_chat_exporter.py` around lines 235 - 239, Update the test setup
around the send_message calls to retain the message returned by send_message,
then use that message’s id directly in the UPDATE statement instead of selecting
by the latest created_at. Preserve the existing timestamp update and commit
behavior while ensuring the intended inserted row is targeted explicitly.
🤖 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.

Inline comments:
In `@docs/design/radio-studio.md`:
- Around line 325-330: Update the “Agent Chat” section to specify that Radio
Studio routes chat through the existing per-agent AgentChatRouter and AgentLoop,
including safe-point queue draining; do not describe it as an independent
standard chat path, and preserve the listed slash commands and rich response
behavior.
- Around line 184-202: Constrain hackrf_spectrum_survey inputs in the
tool_schema and mirror those checks in the API and skill implementation: require
positive start_hz and stop_hz with start_hz < stop_hz, enforce supported gain
bounds, require a positive bin_width_hz, and cap frequency span and resulting
output size. Reject invalid or resource-heavy requests before dispatch using
shared limit definitions where available.
- Around line 285-288: Update the Phase 1 UI documentation around the Spectrum
View and Signal Library layout to remove “Demod actions” or clearly mark it
disabled and explicitly Phase 2, and apply the same treatment to the chat’s
/stare and /capture controls. Ensure targeted sweeps, audio capture, and
demodulation are not presented as available Phase 1 functionality.
- Around line 334-343: Expand the radio-studio API design around POST
/api/radio-studio/survey to define the survey job contract: response payload and
job identifier, states exposed by GET /api/radio-studio/survey/{id}, failure
representation, retry behavior, and idempotency rules. Ensure the contract
supports safe client polling and clarifies how duplicate submissions are
handled.
- Around line 74-76: Keep HardwareProfile.profile_id stable and independent of
SDR detection; remove the -sdr suffix behavior while retaining SDR data through
the separate HardwareProfile.sdr capability. Ensure HardwareProfile.save() and
exact profile_id cache/tier lookups continue using the unchanged base
identifier.
- Around line 349-357: Update the Radio Studio design to define deny-by-default
permissions for skills, specify device policy enforcement that blocks direct USB
TX endpoint access, and add negative tests covering transmit requests and TX
writes. Retain the existing UI/API rejection and receive-only CLI behavior while
documenting concrete container or host restrictions that enforce the policy.
- Around line 110-113: Update the SDR device deployment flow described around
create_container() to pass the detector’s serial value through usb_devices,
using Incus serial selection to identify the intended device. Retain
vendorid/productid as matching metadata, and document busnum/devnum only as
fallback selectors rather than the primary device-selection mechanism.

In `@scripts/check_store_wiring.py`:
- Around line 279-285: Update the new-class detection loop around
_class_def_in_added_lines so renamed files (status starting with “R”) use
added-definition-line detection instead of marking every class as new; retain
unconditional detection for added/copied files and existing modified-file
behavior. Add a regression test for a rename-only change that preserves the
existing store class and confirms no subclass is reported.

In `@tests/conftest.py`:
- Around line 359-362: Update the client fixture teardown to close task_strikes
after project_task_store.close() and before releasing dependent resources. Reuse
the existing task_strikes cleanup pattern, ensuring the connection is closed
only when task_strikes._db is not None.

In `@tinyagentos/agent_chat_router.py`:
- Around line 89-93: Make the cleanup in _run_acp_turn cancellation-safe: ensure
cancellation cannot interrupt reach_safe_point before the AgentLoop state
returns to IDLE, such as by running the drain in a detached task or using a
synchronous reset on AgentLoop. Preserve queued-message draining and update the
finally path accordingly, then add a test that cancels _run_acp_turn during
drive_turn and asserts the a1 loop reaches LoopState.IDLE.

In `@tinyagentos/chat/chat_exporter.py`:
- Around line 229-246: The truncation in _serialized_size/envelope construction
must account for JSON escape expansion: after applying the initial byte budget,
repeatedly shrink emitted_body and re-check _serialized_size(envelope) until it
is within _max_message_bytes, preserving valid UTF-8 and the truncation marker.
In tests/test_chat_exporter.py lines 649-670, add coverage using quotes,
backslashes, or control characters and assert the serialized envelope remains
within the limit.

In `@tinyagentos/projects/lists_store.py`:
- Around line 263-281: Add a per-connection async lock for the shared database
connection and use it to serialize every write unit in ProjectListEntriesStore,
including each execute and its corresponding commit or rollback. Update
reorder_entries() and all other write methods to acquire the same lock across
their full transaction scope, including BaseException cancellation handling, so
writers cannot interleave or affect one another’s pending statements.

In `@tinyagentos/projects/task_store.py`:
- Around line 478-482: Update the unquarantine flow around clear_strikes so a
cleanup failure is not followed by a successful response while stale strikes
remain. When self._strikes.clear_strikes(task_id) fails, preserve or restore the
task’s quarantined status, or persist a retryable cleanup operation, and
propagate the failure through the route’s existing error path.
- Around line 470-473: Update the unquarantine SQL transition in the relevant
task-store method to set claimed_by and claimed_at to NULL alongside status =
'open'. Add a regression test covering a task claimed by one worker,
quarantined, unquarantined, and then successfully claimed by another worker.

In `@tinyagentos/routes/agent_auth_requests.py`:
- Around line 1211-1218: Update the denial flow around the record lookup and
ownership check to apply the same active-state predicate used by
approve_scope_request before allowing denial; inactive or missing identities
must return the existing 404 “agent not found” response. Add a regression test
that suspends an identity with a pending scope request and verifies denial
returns the unknown-target 404.

In `@tinyagentos/routes/auth.py`:
- Around line 657-664: Update AuthManager.validate_session so UA-bound sessions
reject both missing and mismatched User-Agent values, while sessions without
user_agent_hash remain valid. In tinyagentos/routes/auth.py:657-664 and 691-693
retain forwarding; in tinyagentos/routes/canvas.py:82-88,
channel_hub.py:268-274, chat.py:114-120, and terminal.py:26-30 ensure absent
headers are rejected before accepting or starting work. Add absent-User-Agent
coverage for /auth/status and /auth/me in tests/test_auth.py:1224-1263.

In `@tinyagentos/routes/models.py`:
- Line 44: Update delete_model() to locate and remove nested .hef model files
under the documented <backend>/<family>/<id>/<file> layout, rather than only
matching top-level paths with models_dir.glob(). Preserve deletion of the
registry entry and existing handling for other model file suffixes.

---

Outside diff comments:
In `@tests/test_agent_scope_requests.py`:
- Around line 440-462: Update the assertion comment in
test_create_authorizes_before_scope_vocab to state that authorization runs
before scope validation and returns 404, matching the existing resp.status_code
assertion.

---

Minor comments:
In @.github/workflows/secret-ignores-gate.yml:
- Line 30: Disable persisted checkout credentials by setting persist-credentials
to false on the actions/checkout@v7 step in
.github/workflows/secret-ignores-gate.yml at lines 30-30 and
.github/workflows/store-wiring-gate.yml at lines 32-34; apply the same change to
both workflow checkout steps.

In `@desktop/src/apps/agents/RegistryPanel.test.tsx`:
- Around line 800-803: Update the RegistryStatus type used by RegistryEntry and
RegistryStatus handling to accept arbitrary server-provided status strings,
while preserving existing recognized-status behavior. Ensure the "frozen"
fixture in the unrecognised-status test type-checks and follows the existing
Other-section rendering path.

In `@docs/agent-manual/01-rules.md`:
- Line 26: Replace “afterwards” with “afterward” in
docs/agent-manual/01-rules.md:26, then regenerate docs/taos-agent-manual.md so
docs/taos-agent-manual.md:41 reflects the corrected source; do not edit the
generated file independently.

In `@docs/design/radio-studio.md`:
- Line 100: Add language identifiers to all seven flagged fenced code blocks in
the document: use bash for command examples and text for diagrams, routes, and
file maps.

In `@docs/taos-agent-manual.md`:
- Line 45: Update the generated-section formatting in
scripts/build-agent-manual.py so newly emitted prose is separated from the
following `---` by a blank line, or emit an explicit thematic break instead.
Regenerate docs/taos-agent-manual.md and ensure the affected separators no
longer parse the preceding prose as setext headings.

In `@scripts/check_store_wiring.py`:
- Around line 218-221: Exclude the `BaseStore` symbol from the subclass set
returned by the surrounding store-wiring discovery function, while preserving
detection of all other classes that inherit from it. Add a regression test
covering a file defining only `BaseStore` and assert that no unwired subclass is
reported.

In `@tests/test_auth.py`:
- Around line 1256-1259: Update the assertion in the auth client test around the
/auth/me request to require status code 200 for the matching old-browser
User-Agent, rather than merely accepting any non-401 response.

In `@tests/test_chat_context_window.py`:
- Around line 47-55: Update the test around build_context_window to assign
distinct content to each message, such as sequence-labeled values, while
preserving the token-budget setup. Change the final assertion to compare the
retained messages against the expected newest contiguous suffix, ensuring older
messages are dropped first rather than merely checking identical content.

In `@tests/test_chat_exporter.py`:
- Around line 649-670: Extend
test_oversize_envelope_never_exceeds_limit_serialized with JSON-escaping-heavy
content, such as quotes, backslashes, or control characters, instead of relying
only on repeated "x" characters. Keep the existing serialized-envelope length
assertion and ensure the payload exercises expansion between raw content size
and json.dumps output.

In `@tests/test_lxc_installer.py`:
- Around line 791-797: Update test_state_paths_created_in_container to assert
that captured commands include a directory-creation operation targeting
/var/lib/taosr1, rather than merely checking for the path text. Match the
installer’s actual mkdir command structure while preserving the requirement that
creation occurs inside the container.

In `@tests/test_model_manifest_integrity.py`:
- Around line 201-205: Update the download_url validation in the model manifest
integrity check to parse the value as a URL before accepting it. Require a
string value whose parsed scheme is exactly "https" and whose netloc is
non-empty, while preserving the existing error reporting through errors.append
for invalid URLs.

In `@tests/test_routes_mcp.py`:
- Line 44: Update the tuple unpacking in the affected test setup statements to
bind the unused second value to `_` instead of `app`, including both occurrences
around the `app_client` assignments. Keep the `client` binding unchanged.

In `@tinyagentos/chat/chat_exporter.py`:
- Around line 38-56: Update the block-processing function around the existing
dict/string branches to handle every other block type by appending a non-empty
fallback placeholder, rather than silently skipping it. Preserve current
formatting for dict and string blocks, ensuring non-text entries such as numbers
or null remain represented so content_blocks cannot produce an empty body solely
because unsupported types were discarded.
- Around line 146-152: Update the export flow around flatten_body to handle
unsupported content_blocks values without raising ChatExportError. Ensure
flatten_body emits a fallback placeholder for non-dict, non-string blocks such
as numeric values, so channels with content like [123] continue exporting
instead of producing an empty body.

In `@tinyagentos/projects/strike_store.py`:
- Around line 73-87: The list and latest queries in the strike store must use
deterministic ordering when created_at values tie. Update both ORDER BY clauses
in the methods that fetch strikes and latest to add the insertion-order column
as a secondary ascending or descending tie breaker matching each query’s
direction.

---

Nitpick comments:
In `@docs/design/agent-loop-subagents.md`:
- Around line 64-66: Update the diagram code fence containing the IDLE state so
the language tag is attached to the opening fence as text, and remove the
literal text line from the diagram content. Keep IDLE as the first diagram line
and ensure the fence is properly closed.

In `@tests/projects/test_lists_store.py`:
- Around line 400-437: Update test_reorder_entries_rolls_back_on_cancellation to
exercise real task cancellation: run reorder_entries in an asyncio task, pause
the second position update with an event, cancel the task, and assert
CancelledError while verifying the entries were rolled back. Also update the
production rollback handler in reorder_entries to shield the rollback await from
cancellation, adding the required asyncio import.

In `@tests/test_agent_chat_router.py`:
- Around line 820-841: Add an async cancellation test adjacent to
test_run_acp_turn_drive_failure_does_not_wedge_loop, using a blocking fake
drive_turn and cancelling the _run_acp_turn task while it is mid-turn. Await the
cancellation, then assert the agent loop state is IDLE, covering the shutdown
path used by AgentChatRouter.close() and the drain invariant in _run_acp_turn.

In `@tests/test_agent_loop.py`:
- Around line 379-418: Add tests covering the retention limits configured by
AgentLoop._max_subagents and _max_delivered. Exercise spawning beyond
_max_subagents and verify pruning plus which completed handles remain accessible
through get_subagent; also cover delivered-message pruning beyond
_max_delivered, preserving existing status behavior.

In `@tests/test_chat_exporter.py`:
- Around line 235-239: Update the test setup around the send_message calls to
retain the message returned by send_message, then use that message’s id directly
in the UPDATE statement instead of selecting by the latest created_at. Preserve
the existing timestamp update and commit behavior while ensuring the intended
inserted row is targeted explicitly.

In `@tests/test_taos_agent_chat.py`:
- Around line 332-339: Update the fake adapter’s prompt method to store the
result of loop.handle_message in a nonlocal test variable instead of asserting
there; after the supervised response/stream completes, assert that recorded
action is LoopAction.QUEUED from the outer test context so assertion failures
propagate to the test.

In `@tinyagentos/agent_loop.py`:
- Around line 182-190: Update _prune_subagents to remove only the oldest
completed entries, stopping once len(self._subagents) is at or below
_max_subagents. Preserve completed handles until pruning requires their removal,
and retain newer done entries so await_subagent and get_subagent can still
access them.
- Around line 432-443: In the result-waiting logic around the relevant
agent-loop method, remove the redundant cancelled-state condition and return
entry.handle.result through a single path for both already-completed and
newly-completed tasks. Rename the unused done value from asyncio.wait to _done,
and apply the same unused-result rename at the corresponding wait site around
line 481.

In `@tinyagentos/routes/taos_agent.py`:
- Around line 585-589: Update the surrounding generator flow to catch
GeneratorExit explicitly and set a client-disconnected flag, initialized
alongside leftovers; replace the sys.exc_info() check in the warning condition
with that flag, and remove the now-unused sys import.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9dd9c55b-b1e2-464f-9bcb-201b9222ea49

📥 Commits

Reviewing files that changed from the base of the PR and between f78ff3a and aaf1113.

⛔ Files ignored due to path filters (2)
  • desktop/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • uv.lock is excluded by !**/*.lock, !**/uv.lock
📒 Files selected for processing (82)
  • .claude/skills/taos-development-skill/SKILL.md
  • .github/workflows/ci.yml
  • .github/workflows/secret-ignores-gate.yml
  • .github/workflows/store-wiring-gate.yml
  • .gitignore
  • CHANGELOG.md
  • README.md
  • app-catalog/models/deepseek-r1-distill-qwen-1.5b-hef/manifest.yaml
  • app-catalog/models/llama-3.2-3b-instruct-hef/manifest.yaml
  • app-catalog/models/qwen2-1.5b-instruct-hef/manifest.yaml
  • app-catalog/models/qwen2.5-1.5b-instruct-hef/manifest.yaml
  • app-catalog/models/qwen2.5-coder-1.5b-instruct-hef/manifest.yaml
  • desktop/package.json
  • desktop/src/App.tsx
  • desktop/src/apps/GuidesApp.test.tsx
  • desktop/src/apps/SettingsApp.tsx
  • desktop/src/apps/agents/RegistryPanel.test.tsx
  • desktop/src/apps/agents/RegistryPanel.tsx
  • desktop/src/components/Desktop.tsx
  • desktop/src/stores/__tests__/wallpaper-fit.test.ts
  • desktop/src/stores/theme-store.ts
  • desktop/src/theme/tokens.css
  • docs/RELEASING.md
  • docs/agent-coordination.md
  • docs/agent-manual/01-rules.md
  • docs/agent-manual/10-image-prompting.md
  • docs/agent-manual/index.md
  • docs/design/agent-loop-subagents.md
  • docs/design/radio-studio.md
  • docs/mirror-policy.md
  • docs/taos-agent-manual.md
  • pyproject.toml
  • scripts/check_secret_ignores.py
  • scripts/check_store_wiring.py
  • tests/catalog/test_resolver_hailo.py
  • tests/conftest.py
  • tests/projects/test_lists_store.py
  • tests/projects/test_strike_wiring.py
  • tests/test_agent_chat_router.py
  • tests/test_agent_loop.py
  • tests/test_agent_registry.py
  • tests/test_agent_scope_requests.py
  • tests/test_auth.py
  • tests/test_chat_context_window.py
  • tests/test_chat_exporter.py
  • tests/test_check_secret_ignores.py
  • tests/test_check_store_wiring.py
  • tests/test_lxc_installer.py
  • tests/test_model_manifest_integrity.py
  • tests/test_registry.py
  • tests/test_registry_governance_lifecycle.py
  • tests/test_routes_agent_model_api.py
  • tests/test_routes_agent_org.py
  • tests/test_routes_mcp.py
  • tests/test_routes_memory.py
  • tests/test_routes_project_invites.py
  • tests/test_taos_agent_chat.py
  • tests/test_task_store.py
  • tests/test_token_rotation.py
  • tinyagentos/__init__.py
  • tinyagentos/agent_chat_router.py
  • tinyagentos/agent_loop.py
  • tinyagentos/app.py
  • tinyagentos/auth_middleware.py
  • tinyagentos/chat/chat_exporter.py
  • tinyagentos/cluster/capabilities.py
  • tinyagentos/projects/ids.py
  • tinyagentos/projects/lists_store.py
  • tinyagentos/projects/strike_store.py
  • tinyagentos/projects/task_store.py
  • tinyagentos/registry.py
  • tinyagentos/routes/agent_auth_requests.py
  • tinyagentos/routes/agent_registry.py
  • tinyagentos/routes/auth.py
  • tinyagentos/routes/canvas.py
  • tinyagentos/routes/channel_hub.py
  • tinyagentos/routes/chat.py
  • tinyagentos/routes/memory.py
  • tinyagentos/routes/models.py
  • tinyagentos/routes/projects.py
  • tinyagentos/routes/taos_agent.py
  • tinyagentos/routes/terminal.py

Comment on lines +74 to +76
`HardwareProfile` gains an `sdr: SdrInfo = field(default_factory=SdrInfo)` attribute
and `profile_id` gains an `-sdr` suffix when an SDR is present (e.g.
`x86-cuda-16gb-sdr`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(radio-studio\.md|.*hardware.*|.*profile.*)$' | head -80
printf '%s\n' '--- design document ---'
cat -n docs/design/radio-studio.md | sed -n '1,260p'
printf '%s\n' '--- HardwareProfile/profile_id references ---'
rg -n --glob '!docs/design/radio-studio.md' '\bHardwareProfile\b|\bprofile_id\b|SdrInfo|sdr' . | head -300

Repository: jaylfc/taOS

Length of output: 44832


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- implemented HardwareProfile and detection/cache flow ---'
cat -n tinyagentos/hardware.py | sed -n '1,145p'
cat -n tinyagentos/hardware.py | sed -n '580,675p'
printf '%s\n' '--- design profile contract and resolver consumers ---'
cat -n docs/design/plan-hardware-appregistry.md | sed -n '165,225p'
cat -n docs/design/plan-hardware-appregistry.md | sed -n '360,405p'
cat -n docs/design/plan-hardware-appregistry.md | sed -n '600,630p'
printf '%s\n' '--- direct profile/tier consumers in implementation ---'
rg -n -C 4 'hardware_tiers|get_hardware_profile|profile\.profile_id|hw\.profile_id|hardware_profile\.profile_id|detect_hardware\(' \
  tinyagentos app-catalog tests docs/design | head -500

Repository: jaylfc/taOS

Length of output: 46596


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- exact compatibility and profile consumers ---'
cat -n tinyagentos/routes/store.py | sed -n '70,100p'
cat -n tinyagentos/routes/models.py | sed -n '720,765p'
cat -n tinyagentos/routes/dashboard.py | sed -n '118,138p'
cat -n tinyagentos/routes/memory_management.py | sed -n '42,60p'
printf '%s\n' '--- read-only source invariant probe ---'
python3 - <<'PY'
import ast
from pathlib import Path

hardware = Path("tinyagentos/hardware.py").read_text()
tree = ast.parse(hardware)

classes = {
    node.name: node for node in tree.body
    if isinstance(node, ast.ClassDef)
}
hp = classes["HardwareProfile"]
methods = {
    node.name: node for node in hp.body
    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
}
top_level = {
    node.name: node for node in tree.body
    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
}

def calls(func, name):
    return any(
        isinstance(n, ast.Call)
        and ((isinstance(n.func, ast.Name) and n.func.id == name)
             or (isinstance(n.func, ast.Attribute) and n.func.attr == name))
        for n in ast.walk(func)
    )

def has_profile_id_write(func):
    for n in ast.walk(func):
        if not isinstance(n, ast.Assign):
            continue
        for target in n.targets:
            if (isinstance(target, ast.Subscript)
                and isinstance(target.value, ast.Name)
                and target.value.id == "data"
                and isinstance(target.slice, ast.Constant)
                and target.slice.value == "profile_id"):
                return True
    return False

print("HardwareProfile.save writes data['profile_id']:", has_profile_id_write(methods["save"]))
print("HardwareProfile.load removes persisted profile_id:",
      "data.pop(\"profile_id\", None)" in hardware)
print("get_hardware_profile calls detect_hardware:", calls(top_level["get_hardware_profile"], "detect_hardware"))
print("get_hardware_profile calls profile.save:", calls(top_level["get_hardware_profile"], "save"))

radio = Path("docs/design/radio-studio.md").read_text().splitlines()
tier_lines = radio[211:215]
tier_keys = [line.split(":", 1)[0].strip() for line in tier_lines if ":" in line]
print("Radio Studio hardware tier keys:", tier_keys)
print("Radio Studio tier keys contain '-sdr':", any("-sdr" in key for key in tier_keys))
print("Radio Studio specifies re-detection on USB add/remove:",
      any("re-runs `detect_hardware()` on udev `add/remove`" in line for line in radio))
PY

Repository: jaylfc/taOS

Length of output: 5579


Keep SDR detection out of HardwareProfile.profile_id.

HardwareProfile.save() writes profile_id to the hardware cache, while USB re-detection can change the detected SDR state. Compatibility lookups use exact profile_id keys, and the listed hardware_tiers do not include -sdr keys. Keep profile_id stable and expose SDR through a separate capability. If the suffix remains, update all tier manifests, resolvers, cache consumers, and migrations together.

🤖 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 `@docs/design/radio-studio.md` around lines 74 - 76, Keep
HardwareProfile.profile_id stable and independent of SDR detection; remove the
-sdr suffix behavior while retaining SDR data through the separate
HardwareProfile.sdr capability. Ensure HardwareProfile.save() and exact
profile_id cache/tier lookups continue using the unchanged base identifier.

Comment on lines +110 to +113
The container runtime backend exposes a new `usb_devices` argument on
`create_container()` (mirroring the existing `mounts` argument). The SDR
detector returns the matching `vendorid` / `productid` and the app orchestrator
adds the device when deploying an agent that has rf-scanning skills assigned.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target document ---'
sed -n '80,135p' docs/design/radio-studio.md

printf '%s\n' '--- relevant files ---'
git ls-files | rg '(^|/)(radio|sdr|incus|container|device|orchestr|agent)' | head -200

printf '%s\n' '--- symbols and USB fields ---'
rg -n -i --glob '!node_modules' --glob '!dist' --glob '!build' \
  'usb_devices|vendorid|productid|serial|rf-scanning|rf_scanning|create_container|usb' . | head -300

Repository: jaylfc/taOS

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target document ---'
sed -n '80,135p' docs/design/radio-studio.md

printf '%s\n' '--- relevant files ---'
git ls-files | rg -i '(^|/)(radio|sdr|incus|container|device|orchestr|agent)' | head -200

printf '%s\n' '--- USB-related references ---'
rg -n -i --glob '!node_modules' --glob '!dist' --glob '!build' \
  'usb_devices|vendorid|productid|serial|rf-scanning|rf_scanning|create_container|usb' . | head -300

Repository: jaylfc/taOS

Length of output: 50368


🌐 Web query:

Incus USB device type serial vendorid productid busnum devnum device selectors documentation

💡 Result:

In Incus, USB devices can be passed to instances (both containers and virtual machines) using the usb device type [1]. You can identify and select specific USB devices using several configuration keys, which act as selectors [2]. The following configuration keys are available to define or select a USB device [2]: vendorid: The vendor ID of the USB device (e.g., 0c88) [3][2]. productid: The product ID of the USB device (e.g., 163a) [3][2]. serial: The serial number of the USB device [4][2]. busnum: The bus number where the USB device is attached [4][2]. devnum: The device number of the USB device [4][2]. To add a USB device to an instance, use the incus config device add command [5][6]. For example: incus config device add <instance_name> <device_name> usb vendorid=<vendor_id> productid=<product_id> [3][7] Additional context: - For containers, the usb device type is primarily for libusb-compatible devices [1]. Devices requiring dedicated kernel drivers may be better handled via unix-char or unix-hotplug device types [1]. - For virtual machines, the entire USB device is passed through to the instance [1]. - You can also set a device as required (default false) to control whether the instance fails to start if the device is missing [1][2]. - The keys busnum, devnum, and serial were added to allow more granular or stable selection of USB devices [4][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

# Read-only checks for the exact claim: whether this design drops a device
# identity field and whether the repository documents Incus USB selectors.
rg -n -C 5 -i \
  'serial|vendorid|productid|busnum|devnum|usb_devices|create_container|rf-scanning' \
  docs/design/radio-studio.md README.md docs 2>/dev/null | head -400

printf '%s\n' '--- all tracked radio-studio references ---'
rg -n -i 'radio-studio|radio studio|SDR|software-defined radio|rf-scanning' \
  --glob '!docs/design/radio-studio.md' . | head -250

Repository: jaylfc/taOS

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- design excerpt ---'
nl -ba docs/design/radio-studio.md | sed -n '100,120p'

printf '%s\n' '--- candidate tracked source files ---'
git ls-files | rg -i '(^|/)(sdr|radio|usb|incus|container|orchestr|agent)([^/]*)$|(^|/)[^/]*(sdr|radio|usb|incus|container|orchestr|agent)[^/]*\.(go|rs|py|ts|tsx|js|jsx|md|yaml|yml)$' | head -200

printf '%s\n' '--- exact tracked references outside generated/vendor content ---'
git grep -n -I -E 'usb_devices|vendorid|productid|serial|rf-scanning|rf_scanning|create_container' -- \
  ':!**/node_modules/**' ':!**/dist/**' ':!**/build/**' ':!**/*.min.js' \
  ':!**/*.map' ':!**/static/**' ':!**/public/**' | head -300

Repository: jaylfc/taOS

Length of output: 50367


Pass the SDR serial to create_container(). vendorid and productid can match multiple devices. A replacement or second matching device can attach to the agent. Use Incus serial selection and document busnum/devnum only as a fallback.

🤖 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 `@docs/design/radio-studio.md` around lines 110 - 113, Update the SDR device
deployment flow described around create_container() to pass the detector’s
serial value through usb_devices, using Incus serial selection to identify the
intended device. Retain vendorid/productid as matching metadata, and document
busnum/devnum only as fallback selectors rather than the primary
device-selection mechanism.

Source: MCP tools

Comment on lines +184 to +202
tool_schema:
name: hackrf_spectrum_survey
description: "Sweep a frequency range and return FFT bins with power levels"
input_schema:
type: object
properties:
start_hz:
type: integer
description: "Start frequency in Hz"
stop_hz:
type: integer
description: "Stop frequency in Hz"
gain:
type: integer
description: "LNA / VGA gain"
bin_width_hz:
type: integer
default: 100000
required: [start_hz, stop_hz]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bound survey inputs before dispatch.

The schema accepts arbitrary integers. It does not constrain positive frequencies, start_hz < stop_hz, gain limits, bin width, maximum span, or output size. Add schema constraints and enforce the same limits in the API and skill implementation to prevent invalid or resource-heavy sweeps.

🤖 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 `@docs/design/radio-studio.md` around lines 184 - 202, Constrain
hackrf_spectrum_survey inputs in the tool_schema and mirror those checks in the
API and skill implementation: require positive start_hz and stop_hz with
start_hz < stop_hz, enforce supported gain bounds, require a positive
bin_width_hz, and cap frequency span and resulting output size. Reject invalid
or resource-heavy requests before dispatch using shared limit definitions where
available.

Comment on lines +285 to +288
| Spectrum View (canvas) | Scan Log | Signal Library |
| - Waterfall (time vs freq) | - Timestamp | - Saved signals |
| - Spectrum line (current FFT) | - Freq span | - Notes / tags |
| - Cursor / click to tune | - Peaks | - Demod actions |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove Phase 2 actions from the Phase 1 UI.

The layout advertises “Demod actions,” and the chat exposes /stare and /capture. The Phase 1 cut excludes targeted sweeps, audio capture, and demodulation. Remove these controls or mark them disabled and explicitly Phase 2.

Also applies to: 325-328

🤖 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 `@docs/design/radio-studio.md` around lines 285 - 288, Update the Phase 1 UI
documentation around the Spectrum View and Signal Library layout to remove
“Demod actions” or clearly mark it disabled and explicitly Phase 2, and apply
the same treatment to the chat’s /stare and /capture controls. Ensure targeted
sweeps, audio capture, and demodulation are not presented as available Phase 1
functionality.

Comment on lines +325 to +330
**Agent Chat**
- Standard chat pane, scoped to the studio's agent.
- Slash commands: `/survey 1-6G`, `/stare 144.39M`, `/alert 118-137M -60`,
`/capture 30s 144.39M`.
- Agent can push spectrum snapshots and signal cards into the chat as images /
structured data.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Route Radio Studio chat through the existing AgentLoop.

“Standard chat pane” does not identify the serialization owner. Specify that Radio Studio uses the existing per-agent AgentChatRouter and AgentLoop, including safe-point queue draining. A parallel chat path could race with ACP or desktop turns.

🤖 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 `@docs/design/radio-studio.md` around lines 325 - 330, Update the “Agent Chat”
section to specify that Radio Studio routes chat through the existing per-agent
AgentChatRouter and AgentLoop, including safe-point queue draining; do not
describe it as an independent standard chat path, and preserve the listed slash
commands and rich response behavior.

Comment on lines +470 to +473
"""UPDATE project_tasks
SET status = 'open', updated_at = ?
WHERE id = ? AND status = 'quarantined'""",
(now, task_id),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear the prior claim when unquarantining.

If quarantine_task receives a claimed task, it preserves claimed_by. This update restores status = 'open' but also preserves that claim. claim_task requires claimed_by IS NULL, so the task cannot re-enter the claimable pool.

Clear claimed_by and claimed_at in this transition. Add a regression test for claimed → quarantined → unquarantined → claimed by another worker.

Proposed fix
- SET status = 'open', updated_at = ?
+ SET status = 'open', claimed_by = NULL, claimed_at = NULL, updated_at = ?
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""UPDATE project_tasks
SET status = 'open', updated_at = ?
WHERE id = ? AND status = 'quarantined'""",
(now, task_id),
"""UPDATE project_tasks
SET status = 'open', claimed_by = NULL, claimed_at = NULL, updated_at = ?
WHERE id = ? AND status = 'quarantined'""",
(now, task_id),
🤖 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/projects/task_store.py` around lines 470 - 473, Update the
unquarantine SQL transition in the relevant task-store method to set claimed_by
and claimed_at to NULL alongside status = 'open'. Add a regression test covering
a task claimed by one worker, quarantined, unquarantined, and then successfully
claimed by another worker.

Comment on lines +478 to +482
if self._strikes is not None:
try:
await self._strikes.clear_strikes(task_id)
except Exception:
logger.warning("clear_strikes failed for task %s on unquarantine", task_id, exc_info=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not report success when strike cleanup fails.

The task is committed as open before clear_strikes() runs. If cleanup fails, this code only logs the error. The route then returns success with stale strikes still attached. A later verifier can apply the threshold using those stale strikes.

Keep the task quarantined, compensate the status change, or persist a retryable cleanup operation when strike clearing fails.

🤖 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/projects/task_store.py` around lines 478 - 482, Update the
unquarantine flow around clear_strikes so a cleanup failure is not followed by a
successful response while stale strikes remain. When
self._strikes.clear_strikes(task_id) fails, preserve or restore the task’s
quarantined status, or persist a retryable cleanup operation, and propagate the
failure through the route’s existing error path.

Comment on lines +1211 to 1218
logger.info("scope request deny 404-unknown for %s", canonical_id)
raise HTTPException(status_code=404, detail="agent not found")
if not (user.is_admin or user.user_id == record["user_id"]):
logger.info(
"scope request deny 404-not-owner for %s by %s",
canonical_id, user.user_id,
)
raise HTTPException(status_code=404, detail="agent not found")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject inactive identities before a denial.

Line 1210 checks only for a missing record. An owner or admin can therefore deny a pending request after the identity becomes suspended or revoked. approve_scope_request rejects inactive identities, and the existence-hiding contract requires inactive targets to return 404.

Use the same active-state predicate as approval. Add a regression test that suspends an identity with a pending request, then verifies that denial returns the unknown-target 404 response.

Proposed fix
-    if record is None:
-        logger.info("scope request deny 404-unknown for %s", canonical_id)
-        raise HTTPException(status_code=404, detail="agent not found")
+    if record is None or record.get("status") != "active":
+        logger.info("scope request deny 404-unknown-or-inactive for %s", canonical_id)
+        raise HTTPException(status_code=404, detail="agent not found or not active")
🤖 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/routes/agent_auth_requests.py` around lines 1211 - 1218, Update
the denial flow around the record lookup and ownership check to apply the same
active-state predicate used by approve_scope_request before allowing denial;
inactive or missing identities must return the existing 404 “agent not found”
response. Add a regression test that suspends an identity with a pending scope
request and verifies denial returns the unknown-target 404.

Comment on lines +657 to +664
# Pass the request's User-Agent so the stolen-cookie binding check runs
# here exactly as it does in the API middleware. Without it a session
# whose UA hash no longer matches (browser auto-update rotated the UA)
# reads authenticated here while every /api/* call 401s, and the SPA's
# LoginGate remount-loops on that contradiction (the beta.46 PWA
# refresh-loop, 2026-08-10).
_ua = request.headers.get("user-agent", "")
user_id = auth_mgr.validate_session(token, user_agent=_ua) if token else 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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject an absent User-Agent for UA-bound sessions.

Each route converts a missing User-Agent header to "". AuthManager.validate_session then skips comparison because it checks if stored_ua and user_agent. A caller with a stolen UA-bound cookie can omit the header and bypass the binding check.

Update the shared validator. If stored_ua exists, reject both a missing User-Agent and a mismatched User-Agent. Keep sessions without user_agent_hash valid for backward compatibility.

  • tinyagentos/routes/auth.py#L657-L664: retain User-Agent forwarding after the shared validator rejects empty values.
  • tinyagentos/routes/auth.py#L691-L693: retain User-Agent forwarding after the shared validator rejects empty values.
  • tinyagentos/routes/canvas.py#L82-L88: ensure an absent User-Agent closes a UA-bound session before websocket.accept().
  • tinyagentos/routes/channel_hub.py#L268-L274: ensure an absent User-Agent closes a UA-bound session before connector creation.
  • tinyagentos/routes/chat.py#L114-L120: ensure an absent User-Agent closes a UA-bound session before websocket.accept().
  • tinyagentos/routes/terminal.py#L26-L30: ensure an absent User-Agent rejects a UA-bound session before the PTY process starts.
  • tests/test_auth.py#L1224-L1263: add absent-User-Agent cases for /auth/status and /auth/me.
Proposed shared-validator fix
-        if stored_ua and user_agent:
-            if not secrets.compare_digest(
-                stored_ua, hashlib.sha256(user_agent.encode()).hexdigest()
-            ):
-                return None
+        if stored_ua:
+            if not user_agent or not secrets.compare_digest(
+                stored_ua, hashlib.sha256(user_agent.encode()).hexdigest()
+            ):
+                return None
📍 Affects 6 files
  • tinyagentos/routes/auth.py#L657-L664 (this comment)
  • tinyagentos/routes/auth.py#L691-L693
  • tinyagentos/routes/canvas.py#L82-L88
  • tinyagentos/routes/channel_hub.py#L268-L274
  • tinyagentos/routes/chat.py#L114-L120
  • tinyagentos/routes/terminal.py#L26-L30
  • tests/test_auth.py#L1224-L1263
🤖 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/routes/auth.py` around lines 657 - 664, Update
AuthManager.validate_session so UA-bound sessions reject both missing and
mismatched User-Agent values, while sessions without user_agent_hash remain
valid. In tinyagentos/routes/auth.py:657-664 and 691-693 retain forwarding; in
tinyagentos/routes/canvas.py:82-88, channel_hub.py:268-274, chat.py:114-120, and
terminal.py:26-30 ensure absent headers are rejected before accepting or
starting work. Add absent-User-Agent coverage for /auth/status and /auth/me in
tests/test_auth.py:1224-1263.



_MODEL_FILE_SUFFIXES = (".gguf", ".rkllm", ".bin", ".safetensors", ".onnx")
_MODEL_FILE_SUFFIXES = (".gguf", ".rkllm", ".bin", ".safetensors", ".onnx", ".hef")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Delete nested HEF model files.

Line 44 makes nested .hef files visible in get_downloaded_models(). delete_model() only searches the top level with models_dir.glob(f"{model_id}*"). It does not find files in the documented <backend>/<family>/<id>/<file> layout. A user can delete the registry entry but retain the multi-gigabyte HEF download.

Proposed fix
-    for f in models_dir.glob(f"{model_id}*"):
+    for f in models_dir.rglob("*"):
         # Use the same canonical suffix set (case-insensitive) the scan uses,
         # so .safetensors/.onnx models aren't left orphaned on disk after a
         # "delete" — the narrower hardcoded list missed them.
-        if f.is_file() and f.suffix.lower() in _MODEL_FILE_SUFFIXES:
+        rel = f.relative_to(models_dir)
+        belongs_to_model = f.name.startswith(model_id) or model_id in rel.parts
+        if (
+            f.is_file()
+            and belongs_to_model
+            and f.suffix.lower() in _MODEL_FILE_SUFFIXES
+        ):
             f.unlink()
             deleted.append(f.name)
🤖 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/routes/models.py` at line 44, Update delete_model() to locate and
remove nested .hef model files under the documented
<backend>/<family>/<id>/<file> layout, rather than only matching top-level paths
with models_dir.glob(). Preserve deletion of the registry entry and existing
handling for other model file suffixes.

pass

diff = _run_git(["diff", f"{base_ref}...HEAD", "--", file_path], repo_root)
pattern = re.compile(rf"^\+.*class\s+{re.escape(class_name)}\s*\(", re.MULTILINE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CRITICAL: Regex ^\+.* matches +++ diff headers

The pattern ^\+.*class\s+... matches the unified-diff header line +++ b/tinyagentos/<class_name>_store.py for any modified store file whose class name appears in its own filename. A pre-existing store class like MetricsStore in metrics_store.py would be falsely flagged as "newly added" on any PR that touches the file, producing a permanent false-positive CI failure.

Suggested change
pattern = re.compile(rf"^\+.*class\s+{re.escape(class_name)}\s*\(", re.MULTILINE)
pattern = re.compile(rf"^\+[^+].*class\s+{re.escape(class_name)}\s*\(", re.MULTILINE)

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

@@ -1189,8 +1208,14 @@ async def deny_scope_request(
registry = _get_registry_store(request)
record = await registry.get(canonical_id)
if record 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: Missing active-status guard — inconsistent with approve_scope_request

approve_scope_request (line 1084) rejects inactive agents with 404, but deny_scope_request only checks record is None and owner/admin. A scope request for a suspended agent can still be denied by its owner while approval is blocked, creating a behavioral gap between the two endpoints.


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

await notifs.emit_event(
"task.unquarantined", "Task unquarantined", f"{task_id} unquarantined by {actor_id}"
)
return await store.get_task(task_id)

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: unquarinate_task response omits strike metadata

The get_task route enriches the task dict with strike_count and latest_strike. unquarantine_task returns store.get_task(task_id) directly without adding these fields, so clients expecting strike metadata on all task-detail responses see it missing here.


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

if self._strikes is not None:
try:
await self._strikes.clear_strikes(task_id)
except Exception:

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: except Exception misses asyncio.CancelledError in unquarantine_task

The new unquarantine_task catches except Exception around clear_strikes(), but CancelledError is a BaseException, not an Exception. If the coroutine is cancelled during strike clearing, the exception propagates unhandled. The sibling reorder_entries in lists_store.py correctly uses except BaseException for the same cancellation-safety reason.

Suggested change
except Exception:
except BaseException:

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

# half-applied reorder. BaseException, not Exception: task
# cancellation (CancelledError) must also roll back, and commit()
# itself is inside the guard for the same reason.
await self._db.rollback()

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: rollback() exception may mask the original exception in reorder_entries

The except BaseException handler calls await self._db.rollback() and then raise. If rollback() itself raises, the original exception (e.g. CancelledError or a database error) is lost, making debugging transaction failures significantly harder.


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

Comment thread tinyagentos/registry.py
hardware_tiers=data.get("hardware_tiers", {}),
config_schema=data.get("config_schema", []),
variants=data.get("variants", []),
context_window=data.get("context_window", 0),

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: data.get("context_window", 0) returns None when the YAML manifest contains context_window: null

The field is typed as int, but dict.get returns the stored value (including None) when the key exists. A manifest author could legitimately write context_window: null, causing a TypeError in downstream consumers that perform arithmetic or string formatting.


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

return True
# Agent-model surface — consent-key auth lives in the route (see the
# constants block above). Exact paths only; /v1/anything-else stays gated.
if method == "GET" and path == _AGENT_MODEL_MODELS:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Exact-path matching does not exempt /v1/models/ (trailing slash)

The middleware exemption uses path == "/v1/models" and path == "/v1/chat/completions". A request to /v1/models/ with a trailing slash does not match and falls through to the session gate, returning 401 before the route's consent-key check runs.


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

Comment thread tinyagentos/registry.py
hardware_tiers=data.get("hardware_tiers", {}),
config_schema=data.get("config_schema", []),
variants=data.get("variants", []),
context_window=data.get("context_window", 0),

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: data.get("context_window", 0) returns None when the YAML manifest contains context_window: null

The field is typed as int, but dict.get returns the stored value (including None) when the key exists. A manifest author could legitimately write context_window: null, causing a TypeError in downstream consumers that perform arithmetic or string formatting.


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

return True
# Agent-model surface — consent-key auth lives in the route (see the
# constants block above). Exact paths only; /v1/anything-else stays gated.
if method == "GET" and path == _AGENT_MODEL_MODELS:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Exact-path matching does not exempt /v1/models/ (trailing slash)

The middleware exemption uses path == "/v1/models" and path == "/v1/chat/completions". A request to /v1/models/ with a trailing slash does not match and falls through to the session gate, returning 401 before the route's consent-key check runs.


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

@kilo-code-bot

kilo-code-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 7 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 5
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue
scripts/check_store_wiring.py 96 Regex ^\+.* matches +++ diff headers, causing false-positive CI failures for any store class whose filename contains its class name

WARNING

File Line Issue
tinyagentos/routes/agent_auth_requests.py 1210 deny_scope_request lacks active-status guard present in approve_scope_request
tinyagentos/routes/projects.py 1083 unquarantine_task response omits strike_count and latest_strike fields
tinyagentos/projects/task_store.py 481 except Exception misses asyncio.CancelledError in unquarantine_task
tinyagentos/projects/lists_store.py 280 rollback() exception in reorder_entries may mask the original exception
tinyagentos/registry.py 79 context_window=data.get("context_window", 0) returns None for explicit null in manifest

SUGGESTION

File Line Issue
tinyagentos/auth_middleware.py 420 Exact-path matching does not exempt /v1/models/ (trailing slash)
Files Reviewed (7 files)
  • scripts/check_store_wiring.py - 1 issue
  • tinyagentos/routes/agent_auth_requests.py - 1 issue
  • tinyagentos/routes/projects.py - 1 issue
  • tinyagentos/projects/task_store.py - 1 issue
  • tinyagentos/projects/lists_store.py - 1 issue
  • tinyagentos/registry.py - 1 issue
  • tinyagentos/auth_middleware.py - 1 issue

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 218.9K · Output: 30K · Cached: 7.4M

@jaylfc

jaylfc commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Superseded by the sync-branch promotion PR - master's 3 direct Dependabot merges made dev BEHIND under the strict up-to-date rule; same resolution as beta.45/46 (sync branch = dev + merge of master).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants