Skip to content

release: promote dev to master (v1.0.0-beta.47) - #2320

Merged
jaylfc merged 64 commits into
masterfrom
dev
Aug 9, 2026
Merged

release: promote dev to master (v1.0.0-beta.47)#2320
jaylfc merged 64 commits into
masterfrom
dev

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Promotes dev to master for the v1.0.0-beta.47 release. See CHANGELOG's 1.0.0-beta.47 section for contents (10 merges today: device pairing consent loop, device block/revoke, registry token rotation, cryptography 50.0.0, auth session pruning, Library settings scaffolding, doc-gate fixes, consent-defer 409, SPA deps, plus the accumulated Unreleased items). Tag + GitHub release follow the merge.

Summary by CodeRabbit

  • New Features

    • Added device pairing requests with approval, denial, expiry, and device blocking controls.
    • Added project notes with create, edit, delete, and project-scoped access.
    • Enhanced chat rendering for thinking, status, questions, and tool activity.
    • Added live “Agent is thinking” indicators in channels.
    • Added Library settings for download quality, storage limits, and source rules.
    • Renamed Tasks to Routines throughout the app.
  • Bug Fixes

    • Reduced-effects mode now suppresses animated particle wallpapers.
    • Improved session cleanup and token rotation controls.
    • Strengthened request protection and consistent API headers.

jaylfc and others added 30 commits August 2, 2026 10:43
…otifications (#2280)

* feat(notifications): admin-gated POST /api/notifications to create notifications

Add an ADMIN-gated POST /api/notifications accepting
{title, message, level=info, source, data} that calls
NotificationStore.add() so SSE and PWA web-push fire for review-request
notifications. Previously notifications were only creatable via the
internal store, forcing orchestrators and lead agents to do raw DB
inserts that skip SSE and web-push.

Includes a test asserting non-admin sessions get 403 and that an
admin-created notification appears in GET /api/notifications.

* docs(changelog): admin notification-create endpoint
…6) (#2275)

* tsk-3lkyq2  [OPEN]  taOStalk s1: ToolCallBlock + StatusBlock/question

* docs(changelog): tool_call and status content-block renderers
* taOStalk s1: show live thinking badge on bound session channels

- add taostalk_agent to Channel settings type
- compute thinkingChannelIds from channels whose taostalk_agent matches
  a currently thinking agent in the existing typingAgents state
- pass thinkingChannelIds to ChannelSidebar
- render a pulsing amber dot next to thinking session channels in both
  mobile and desktop sidebar rows
- tests: badge appears on thinking start, absent otherwise, clears on end

* a11y(chat): give the thinking badge an accessible name, plus changelog

The badge was aria-hidden with a title attribute, so the one piece of
information it conveys - that an agent is working - was unavailable to
screen readers, and title is not exposed when aria-hidden removes the
element from the accessibility tree. role=img with an aria-label names it
without turning every channel row into a live region.
* tsk-6ylbbx  [OPEN]  taOStalk s1: TextBlock + ThinkingBlock renderers (

* docs(changelog): text and thinking content-block renderers

* fix(chat): use the shell surface token for the thinking toggle hover

MessagesApp.palette.test.ts forbids hardcoded white/black utilities in this
file; the new disclosure button used hover:bg-white/[0.06]. Swapped for
hover:bg-shell-surface-hover, the token the rest of the file already uses,
rather than adding a palette-ok waiver - the waiver would silence the check
instead of satisfying it.
* Add a NOTES area to the Projects app - a place for the user (and agent

* docs: project notes area, project_notes scope, and its agent API surface
…etch wrappers (#2289)

* fix(desktop): add CSRF protection to mutating requests in library, framework-api, memory-api, x-monitor

Four lib modules were using raw fetch() on mutating HTTP methods
(POST/PUT/DELETE) without the withCsrf wrapper, bypassing the
double-submit CSRF check.

Affected functions:
- library.ts: deleteLibraryItem, reprocessLibraryItem, ingestLibraryUrl
- framework-api.ts: startFrameworkUpdate, setPermittedModels
- memory-api.ts: setMemoryModel
- x-monitor.ts: postJson/putJson helpers, deleteWatch

Fixes #2126

* fix(desktop): install auth guard in chat and standalone app PWAs

chat-main.tsx and app-standalone-main.tsx were missing installAuthGuard(),
leaving the chat PWA and standalone app PWA without CSRF protection on
window.fetch calls. Only main.tsx (desktop shell PWA) had the guard.

Add the import + call to both entry modules, matching the pattern in
main.tsx. installAuthGuard is idempotent (module-level flag), so double
install is safe if an entry point is loaded more than once in the same
realm.

Add entry-guards.test.ts asserting all three entry modules install the
auth guard on import.

* fix(desktop): normalise Headers instance in x-monitor fetchJson, add regression test

withCsrf() returns a Headers instance at csrf.ts:33.  Spreading that
instance into an object literal (the old fetchJson line 56 pattern)
silently drops every entry because Headers entries are not own-
enumerable properties.  The CSRF token the PR was written to add was
therefore never reaching the wire.

Fix by building a fresh Headers with the Accept default and copying
caller headers via forEach — the iterator visits all entries correctly.

Add a regression test that exercises createWatch → postJson → fetchJson
→ withCsrf with a stubbed csrf_token cookie, asserting the token
survives into the final fetch call.

* fix(desktop): preserve Headers instance in fetchJson wrappers

Do not spread init.headers into a plain object literal — this drops
multi-value header support and breaks Headers-method access. When
withCsrf() returns a live Headers instance, the old pattern silently
discarded the X-CSRF-Token header because Headers objects have no
enumerable own properties.

Instead construct a fresh Headers from init?.headers (handles both
plain-object and Headers inputs), set Accept, and pass the Headers
directly to fetch().

Also fixes fetchJsonOrThrow in memory.ts and standardises x-monitor.ts
to match the same pattern.

Closes CodeRabbit finding on PR #2165.

* fix(test): update header assertions for Headers instance (not plain object)

The Headers-spread fix in the wrapper modules now passes native Headers
objects to fetch() instead of plain {Accept: ...} objects. Tests that
accessed opts.headers.Accept fail because Headers.get() is required.
Updated all 24 assertions across 7 test files.

* fix(test): handle both Headers and plain-object in Accept assertions

Some fetch wrappers (fetchJson) now pass Headers instances, but direct
fetch() calls still use plain objects. Use opts.headers?.get?.("Accept")
?? opts.headers?.Accept to handle both cases.

* fix(test): use dual-mode header assertions for Content-Type in github/knowledge/memory

The CSRF fix converted fetch wrappers to pass Headers instances (see
2273290). a7cf0af updated the Accept assertions to the dual-mode
pattern (opts.headers?.get?.() ?? opts.headers?.[...]) but left the
Content-Type assertions reading a plain-object index, so the six
postJson/putJson tests received undefined. Apply the same dual-mode
pattern to Content-Type so Headers-instance and plain-object callers
both resolve.

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>
Every agent runs the same scripts from ~/.taos-team/, so a pattern kill is a
fleet-wide kill. It fired in both directions in twelve hours on 2026-08-04:
three watcher deaths, two wrong diagnoses, each agent having checked whether a
process looked like theirs rather than whose it was. Records the owner check
(env, not cmdline) and the three ways of writing it that silently do not work.

Docs-Reviewed: this IS the coordination doc; the rule it adds is the doc change.
* tsk-fgj5yp  [OPEN]  Rename Tasks app to Routines (#2143)

* fix(docs): rename Tasks in the manual SOURCE, not the generated file

The PR edited docs/taos-agent-manual.md, which is generated from
docs/agent-manual/*.md by scripts/build-agent-manual.py. The source still said
Tasks, so test_compiled_output_matches_committed failed and the next person to
run the build would have silently reverted the rename. Fixed the source and
regenerated. Changelog added as a fragment.
….http (#2299)

projectsApi.http built its default headers with a plain-object spread
(...(init?.headers || {})). When init.headers was a Headers instance
(as returned by withCsrf downstream), the spread silently dropped every
entry, and the trailing ...init spread overwrote the merged headers key
entirely, clobbering Content-Type. Mirror the x-monitor/library fix:
build a fresh Headers via new Headers(init?.headers), set Content-Type
on it, and spread ...init before headers so merged headers win.
…2304)

Add desktop/src/App.tsx, desktop/src/components/**, and desktop/src/stores/**
to the when_changed glob for the user-visible-changelog rule so modifications
to the desktop shell require a changelog entry, matching the existing coverage
for desktop/src/apps/*/**.
An unparseable or missing doc-gate config previously raised an unhandled
traceback that exited 1 -- identical to a real documentation-drift violation,
so a typo in docs/doc-gate.toml was indistinguishable from a missing
changelog. Catch tomllib.TOMLDecodeError and OSError in main(), print a
clear error to stderr, and exit 2 instead.
…2283)

* docs: add taos-agent OS skill for OS-native agent operation

Add .claude/skills/taos-agent/SKILL.md consolidating the agent-manual
OS-operation content into actionable instructions for the OS-native taOS
agent. Features the hard rule that all desktop driving goes only through
POST /api/desktop/command + POST /api/desktop/screenshot. Covers opening
and driving apps and windows, desktop control, projects, files, memory and
notes, chat conventions, image generation, and answering the user.

Updates docs/agent-manual/index.md to point at the new skill and at the
existing taos-development-skill. Adds a CHANGELOG entry.

Draft for @taOS-dev review.

* docs(skill): state that desktop control needs a session, not a registry JWT

* ci: re-trigger checks on the current head

doc-gate's failure was recorded at 11:16Z against the PREVIOUS head; the rebase
at 41dce53 landed after it and doc-gate never re-reported, leaving a stale
required check blocking the PR. Verified clean locally against current dev.

* docs: reword 'desktop/window control' so the invariants scan stops reading it as a path

check_doc_gate.py's Layer A scans index.md for referenced repo paths and treated
the slash in 'desktop/window control' as a path that does not exist. It is
prose, not a path. 'desktop and window control' reads better anyway.
…ard)

_do_approve never passed defer_binding to approve_request_record, so the
flag added in PR 2187 was dead code: the project_id-required 400 guard
still fired on the defer combo, and when project_id was present the token
was bound anyway despite defer_binding:true.

- Pass defer_binding=body.defer_binding at the call site.
- Add a 400 guard for defer_binding:true with an explicit (non-blank)
  project_id -- the two are contradictory.
- Route tests covering the defer combo (unbound token, unbound grants,
  no membership row, no a2a channel), the 400 guard, and the unchanged
  non-deferred path.

Red run on buggy head (before the fix):
  test_defer_with_project_scopes_no_project_id_succeeds_unbound -> 400 (expected 200)
  test_defer_with_explicit_project_id_returns_400 -> 200 (expected 400)

Lifecycle note on never-bound deferred identities:
An unbound deferred identity is minted with project_id=None on both the
token and every grant. It is currently indistinguishable from a global
identity: no expiry, no pending binding flag, no visibility surface in
the Permissions app. A follow-up card should track (a) a lifecycle/expiry
mechanism so deferred-but-never-bound grants do not persist forever, and
(b) a visibility surface so operators can see and reap stale deferred
identities.
…figs

Pr #2306 left two config-error paths that still exited 1 instead of the
config-error code:

- Structurally invalid config (valid TOML, wrong shape, e.g. rules is a
  string) passed load_config and died later in the rule loop with an
  AttributeError.  Add _validate_config to check the shape after parsing.

- Non-UTF-8 config bytes raised UnicodeDecodeError inside tomllib.load
  (which decodes before parsing), uncaught by the TOMLDecodeError-only
  handler.  Catch that alongside the other config errors.

Also move EXIT_CONFIG_ERROR from 2 to 3: exit code 2 is argparse's own
usage-error exit, so a typo'd flag was indistinguishable from a broken
config.  Now 1 (violation), 2 (argparse), and 3 (config error) are all
mutually distinct.

Supersedes exec/tsk-thh54d (pr #2306).
…ctive-handle 409

Documents the behaviour added in this PR: approving with defer_binding when the
agent already holds an active handle returns 409 and points at assign-agent,
rather than advising a duplicate identity.

Clears the two doc-gate rules the PR was failing (user-visible-changelog and
agent-manual).
jaylfc and others added 21 commits August 9, 2026 13:15
…dev/spa-deps-0488a9d012

chore(deps): bump the spa-deps group in /desktop with 12 updates
Add token_min_iat INTEGER column (default 0) to agent_registry so tokens
can be invalidated per-identity without revoking the whole identity.

- migration _migration_v6_add_token_min_iat: idempotent ALTER TABLE with
  default 0 so the migration cannot lock the fleet out
- AgentRegistryStore.bump_token_min_iat(canonical_id, ts): persist cutoff
- Auth path (_verify_agent_scope): after the status check, reject tokens
  whose iat claim is < the record's token_min_iat with 401 'token superseded'
- POST /api/agents/registry/{id}/rotate-tokens: session-owner/admin route
  that bumps the cutoff and writes a forensic audit-log entry
- Tests: store-level (bump sets cutoff, default zero, nonexistent nil),
  auth-path (old token rejected, new token passes, default-zero safe),
  route (admin rotates, owner rotates, nonexistent 404)
- bump_token_min_iat: use MAX(token_min_iat, ?) so the cutoff is
  monotonically non-decreasing — a stale/lower timestamp cannot
  silently un-supersede tokens
- rotate-tokens route: handle None from bump_token_min_iat (race
  condition) with 404 instead of AttributeError on updated.get()
- audit entry: capture before_token_min_iat and after_token_min_iat
  in the governance audit payload so rotation values are traceable
- tests: remove unused token bindings in route-level tests
Docs-Reviewed: CI workflow changes are rebase artifacts (restoring dev's baseline). Token rotation API is internal to agent_registry, not user-visible. Agent behavior unchanged — this is a registry-side enforcement of token freshness.
tsk-mtds32  [OPEN]  S4e: device pair-requests + grant Decision (consen
…shrunk file

Review fixes on the lane's version: the _PersistentSessions copy of the two
prune methods referenced self._sessions_file, an attribute that class does not
have (AttributeError if ever called) - deleted; AuthManager's startup pruner
loaded, pruned a throwaway dict and never wrote back, so the grown file only
shrank on the next session mint - it now loads (which prunes) and saves once,
and the log reports before -> after rather than after -> after. Added the test
that fails on the decorative version: the sessions FILE must shrink at init.

Docs-Reviewed: internal cleanup of an unmerged branch; behaviour is the carded fix itself
Auth sessions never pruned: 37,077 entries, 4.8MB and growing
…t, reuse client fixture

BLOCKER #2: test_owner_can_rotate was a copy of test_admin_can_rotate
— both used the same admin session.  Replaced with three real tests:
- test_nonadmin_owner_can_rotate_own_identity (→200)
- test_nonadmin_cannot_rotate_others_agent (→403)
- test_empty_userid_agent_is_admin_only (→403)

BLOCKER #3: Added TestMigrationV5ExistingDB which builds the pre-change
agent_registry schema by hand (no token_min_iat column), inserts a row,
inits the store, and asserts the row gained token_min_iat=0 with old
tokens still authenticating.

SHOULD-FIX #4: agent_app fixture now depends on the shared client
fixture and only inits agent_registry + agent_grants on top, removing
~200 lines of copy-paste that would break when new stores land in
conftest.
Fix-forward PR 2306 (doc-gate config errors): 2 config-error paths still exit 1
Fix-forward PR 2308 (consent defer): untested defer+active-handle path, 409 advises a duplicate identity
registry: per-identity token rotation via token_min_iat (fixes #2205)
Collate 5 changelog.d fragments into the 1.0.0-beta.47 section (merged with
the direct [Unreleased] entries under single headings), bump the four version
files including uv.lock's normalized 1.0.0b47.

Docs-Reviewed: release version bump; no CI, packaging or contribution rule change
chore(release): v1.0.0-beta.47
@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

github-actions Bot commented Aug 9, 2026

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.

@gitar-bot

gitar-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

Important

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

Gitar

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The release adds backend stores and routes for project notes, device pairing, device blocking, notifications, token rotation, and deferred binding. It also adds desktop chat and library features, request-security updates, documentation, changelog tooling, tests, and beta.47 metadata.

Changes

Backend platform capabilities

Layer / File(s) Summary
Persistent stores and application wiring
tinyagentos/agent_registry_store.py, tinyagentos/device_store.py, tinyagentos/projects/notes_store.py, tinyagentos/device_pair_requests_store.py, tinyagentos/app.py, tinyagentos/auth.py
Adds persistent project notes, pairing requests, blocked-device state, token cutoffs, session pruning, and application lifecycle wiring.
Authentication, notes, devices, and notifications
tinyagentos/routes/*.py, tinyagentos/auth_middleware.py, tinyagentos/agent_token_auth.py
Adds project-notes authorization, device blocking, admin notifications, token rotation, and deferred project binding.
Device pairing workflow
tinyagentos/routes/device_pair_requests.py, tinyagentos/routes/decisions.py
Adds capability-based pairing requests, Decision approval or denial, device registration, expiry handling, and one-time token claiming.
Backend integration and regression validation
tests/**/*.py, tests/conftest.py
Adds coverage for migrations, authorization, pairing, devices, notes, token rotation, notifications, session pruning, and peer-route controls.

Desktop experience updates

Layer / File(s) Summary
Structured chat blocks and activity indicators
desktop/src/apps/MessagesApp.tsx, desktop/src/components/StatusBlock.tsx, desktop/src/components/ToolCallBlock.tsx, desktop/src/apps/chat/*
Adds text, thinking, tool-call, status, and question rendering, plus channel indicators for active agent thinking.
Library settings and visual effects
desktop/src/apps/LibraryApp.tsx, desktop/src/App.tsx, desktop/src/components/Desktop.tsx, desktop/src/apps/*test*
Adds persisted library quality, capacity, and source rules. Reduced-effects mode now suppresses particle wallpaper.
Desktop request headers and CSRF protection
desktop/src/lib/*
Normalizes request headers with Headers, enforces JSON headers, and applies CSRF protection to mutating requests.
Desktop application and entry-point validation
desktop/src/__tests__/*, desktop/src/apps/*test*, desktop/src/components/__tests__/*
Adds tests for authentication guards, activity, media playback, coding studio, project-member renaming, dialogs, and request behavior.
Routines terminology
desktop/src/apps/TasksApp.tsx, desktop/src/registry/app-registry.ts
Renames visible Tasks terminology to Routines while preserving the application ID and behavior.

Documentation and release tooling

Layer / File(s) Summary
taOS agent operating documentation
.claude/skills/taos-agent/SKILL.md, docs/agent-manual/*, docs/taos-agent-manual.md
Documents control-API operation, applications, files, memory, chat, image generation, updates, and prohibited actions.
Release and operational documentation
CHANGELOG.md, docs/agent-coordination.md, docs/gitea.md, docs/design/*, docs/superpowers/*
Updates release notes and documents pairing, notes, deferred binding, process handling, Gitea configuration, Routines, and changelog fragments.
Documentation gate and changelog collation
docs/doc-gate.toml, scripts/check_doc_gate.py, scripts/collate_changelog.py, tests/test_doc_gate.py, tests/test_collate_changelog.py
Adds changelog-fragment support, configuration validation, distinct exit codes, and deterministic release collation.
Version and dependency metadata
desktop/package.json, pyproject.toml, tinyagentos/__init__.py
Advances the project to beta.47 and updates selected dependency minimums and development packages.

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

Sequence Diagram(s)

sequenceDiagram
  participant Device as Pairing device
  participant API as device_pair_requests routes
  participant Store as DevicePairRequestsStore
  participant Decisions as Decisions route
  participant Devices as DeviceStore

  Device->>API: POST /api/device-pair-requests
  API->>Store: create pending request
  API->>Decisions: create device_pairing decision
  Device->>API: GET request status
  Decisions->>Store: set accepted or denied
  Decisions->>Devices: register approved device
  Decisions->>Store: claim scoped token once
  Device->>API: poll accepted request
Loading

Possibly related PRs

  • jaylfc/taOS#2285: Adds the same project-notes stores, routes, middleware, scopes, and tests.
  • jaylfc/taOS#2275: Adds overlapping chat content-block and renderer changes.
  • jaylfc/taOS#2208: Adds the same per-identity token rotation implementation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% 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 and version, matching the pull request objectives.
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.

@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: 8

Note

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

🟡 Minor comments (18)
desktop/src/apps/codingstudio/BuildView.test.tsx-4-4 (1)

4-4: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use vi.mocked for the mocked import.

streamTaosAgentChat is still typed as the production function, so calling streamTaosAgentChat.mockImplementation(...) relies on any. Create a typed mock reference and call mockImplementation on it.

Proposed fix
 import { streamTaosAgentChat } from "../appstudio/stream-chat";
 
+const mockStreamTaosAgentChat = vi.mocked(streamTaosAgentChat);
+
 ...
-    streamTaosAgentChat.mockImplementation(
+    mockStreamTaosAgentChat.mockImplementation(
 ...
-    streamTaosAgentChat.mockImplementation(
+    mockStreamTaosAgentChat.mockImplementation(

Also applies to: 51-52, 83-84

🤖 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/codingstudio/BuildView.test.tsx` at line 4, Update the
BuildView.test.tsx tests to create a typed mock reference for
streamTaosAgentChat using vi.mocked, and call mockImplementation through that
reference instead of the imported production function. Apply the same change to
the additional mocked import usages at the referenced test sections.
docs/agent-coordination.md-69-69 (1)

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

Use a fenced code block for the /proc command.

markdownlint-cli2 reports MD046 at Line 69 because the command uses an indented code block. Replace it with a fenced sh block.

🤖 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-coordination.md` at line 69, Replace the indented code block
containing the /proc environment and kill command in the agent coordination
documentation with a fenced sh code block, preserving the command unchanged.

Source: Linters/SAST tools

desktop/src/apps/LibraryApp.tsx-147-152 (1)

147-152: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate the persisted settings shape and storage cap.

Valid JSON is not necessarily valid LibrarySettings. A negative cap produces a negative progress value. A source-rule field that is an object causes a React render error in the settings table. parseInt also truncates fractional input.

Validate quality, action, source-rule fields, and a finite non-negative integer cap before storing or rendering values. Fall back to defaults for invalid persisted fields.

Also applies to: 1229-1233

🤖 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/LibraryApp.tsx` around lines 147 - 152, Validate each parsed
LibrarySettings field before returning it from the persisted-settings loader,
including preferred quality, action, and source-rule entries. Accept
storage_cap_bytes only when it is a finite, non-negative integer; reject
fractional or negative values. Ensure source_rules and their fields have the
expected shapes before rendering, and fall back to the corresponding
DEFAULT_LIBRARY_SETTINGS values for every invalid field.
desktop/src/apps/chat/ChannelSidebar.tsx-64-65 (1)

64-65: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Propagate thinking state to project channel rows.

thinkingChannelIds only reaches the direct section rows. The desktop project loop and ProjectsSectionMobile do not receive this prop. Active project channels therefore do not show the thinking indicator in standalone Messages.

Pass the IDs into both project-channel renderers and render the same indicator for matching channel IDs.

🤖 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/chat/ChannelSidebar.tsx` around lines 64 - 65, Propagate
thinkingChannelIds from ChannelSidebar into the desktop project-channel loop and
ProjectsSectionMobile, then render the existing thinking indicator for project
rows whose channel ID is included. Preserve the current direct-section behavior
and reuse the established indicator logic and prop names in the project row
renderers.
desktop/src/apps/chat/__tests__/render-helpers.test.tsx-102-116 (1)

102-116: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a valid ToolCallContentBlock fixture.

Line 107 omits required call_id and status. The catch-all ContentBlock member accepts the malformed object, so this test renders ToolCallBlock with no status indicator.

Proposed fix
-        { kind: "tool_call", name: "grep", args: {} },
+        { kind: "tool_call", call_id: "c1", name: "grep", status: "running" },
🤖 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/chat/__tests__/render-helpers.test.tsx` around lines 102 -
116, Update the tool_call fixture in the renderContent test to include valid
ToolCallContentBlock fields, specifically call_id and status, so ToolCallBlock
renders through its normal status-aware path. Keep the existing assertions and
other content fixtures unchanged.
docs/doc-gate.toml-66-66 (1)

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

Remove the duplicate [[rules]] declaration.

Line 66 creates an empty rule table before user-visible-changelog. Remove one declaration so the configuration contains only the intended rule.

🤖 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/doc-gate.toml` at line 66, Remove the duplicate empty [[rules]]
declaration adjacent to user-visible-changelog in doc-gate.toml, leaving only
the intended rule table.
scripts/collate_changelog.py-45-56 (1)

45-56: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject non-bullet fragment content.

parse_fragment accepts titles and ordinary prose as changelog entries. This conflicts with the documented malformed-fragment failure behavior and can publish invalid release notes.

Require a - bullet for each entry. Allow an indented continuation only after a bullet.

🤖 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/collate_changelog.py` around lines 45 - 56, Update parse_fragment to
accept entries only when lines begin with “- ”, rejecting titles or ordinary
prose as malformed fragment content. Track whether a bullet is currently active,
append indented continuation lines only after that bullet, and raise the
existing no-bullets error when no valid bullets are found.
.claude/skills/taos-agent/SKILL.md-59-62 (1)

59-62: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle the screenshot 409 response explicitly.

The guidance describes a missing desktop as delivered == 0, but POST /api/desktop/screenshot returns HTTP 409 when no desktop is connected. State the separate handling for command delivery, screenshot/layout 409 responses, and 504 timeouts.

Suggested wording
- If no desktop is connected (`delivered` is 0) or the desktop did not respond in time (504), report that to the user rather than retrying into a void.
+ For `/api/desktop/command`, report `delivered == 0` as no connected desktop.
+ For screenshot and layout reads, report HTTP 409 as no connected desktop and HTTP 504 as a timeout.
🤖 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 @.claude/skills/taos-agent/SKILL.md around lines 59 - 62, Update the
desktop-communication guidance near the delivered/504 handling to explicitly
distinguish command delivery failures from screenshot or layout requests: treat
delivered == 0 as no connected desktop, report HTTP 409 from screenshot/layout
endpoints as the corresponding unavailable-desktop response, and report HTTP 504
as a timeout; do not retry or use alternate automation paths.
.claude/skills/taos-agent/SKILL.md-113-113 (1)

113-113: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the current display name Routines.

The skill still lists Tasks, while desktop/src/registry/app-registry.ts:63 and both manuals use Routines. Keep the tasks identifier unchanged, but update the user-facing name here.

Suggested fix
- Other bundled apps (Library, Channels, Secrets, Tasks, MCP, Guides and more); if you do not know one, guess from its name and point the user to Guides.
+ Other bundled apps (Library, Channels, Secrets, Routines, MCP, Guides and more); if you do not know one, guess from its name and point the user to Guides.
🤖 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 @.claude/skills/taos-agent/SKILL.md at line 113, Update the user-facing
bundled-app list in the skill text to use “Routines” instead of “Tasks,” while
preserving the underlying tasks identifier unchanged.
.claude/skills/taos-agent/SKILL.md-342-345 (1)

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

Qualify the privacy claims.

State that taOS core storage and built-in update telemetry stay local as described, but data sent through Browser/terminal traffic, cloud providers, or apps can leave the host. Remove or verify the statement about IP retention.

🤖 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 @.claude/skills/taos-agent/SKILL.md around lines 342 - 345, Update the “Is my
data private?” section to qualify its claims: state that taOS core storage and
built-in update telemetry remain local, while Browser/terminal traffic,
configured cloud providers, and apps may send data outside the host. Remove the
unconditional “only two things ever leave” wording and remove or verify any
claim about IP retention.
desktop/package.json-90-90 (1)

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

Raise the Node.js floor to match Vite 8.

desktop/package.json declares Vite 8, whose engines.node is ^20.19.0 || >=22.12.0; CI uses actions/setup-node with node-version: "20", but that does not lock the floor to Vite 8’s minimum. Add engines.node or an .nvmrc/package manager runtime to 20.19+ so contributors and packaging environments cannot install under an incompatible Node.js.

🤖 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/package.json` at line 90, Update the desktop package runtime
requirements alongside the Vite dependency declaration to enforce Node.js
^20.19.0 or >=22.12.0. Prefer adding the package’s engines.node field so
contributors, CI, and packaging environments reject incompatible Node versions.

Source: MCP tools

tinyagentos/routes/agent_registry.py-789-798 (1)

789-798: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Audit the persisted cutoff value.

bump_token_min_iat uses MAX(token_min_iat, ts). If an existing cutoff is later than ts, after_token_min_iat=ts records a value that was not stored. Use updated["token_min_iat"] in the audit payload.

🤖 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_registry.py` around lines 789 - 798, Update the
_audit_governance call in the rotate-tokens flow so after_token_min_iat uses the
persisted cutoff from updated["token_min_iat"] rather than the requested ts
value, preserving the actual MAX-based result in the audit record.
tinyagentos/device_pair_requests_store.py-1-19 (1)

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

Module docstrings are placed after from __future__ import annotations in both new pairing files. A string literal is only the module docstring when it is the first statement in the file. In both files the future import runs first, so the text becomes a discarded expression and __doc__ is None. Tools such as help(), pydoc, and IDE hovers show no documentation for either module.

  • tinyagentos/device_pair_requests_store.py#L1-L19: move the store docstring above from __future__ import annotations.
  • tinyagentos/routes/device_pair_requests.py#L1-L28: move the route docstring above from __future__ import annotations.
🤖 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/device_pair_requests_store.py` around lines 1 - 19, Move the
module docstring before the future import in
tinyagentos/device_pair_requests_store.py lines 1-19 and
tinyagentos/routes/device_pair_requests.py lines 1-28. Ensure each docstring is
the first statement so the corresponding module’s __doc__ and documentation
tools expose it.
tinyagentos/auth.py-60-67 (1)

60-67: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Legacy float session entries are never pruned.

_prune_expired skips any entry that is not a dict. Legacy entries are plain floats holding expires_at (see _get_session_entry at line 671 and cleanup_sessions at line 735). Expired legacy entries therefore stay in the file forever, which defeats the startup shrink for installs that predate the dict shape.

🔧 Proposed fix
     def _prune_expired(self, data: dict) -> None:
         now = time.time()
-        expired_keys = [
-            token for token, entry in data.items()
-            if isinstance(entry, dict) and entry.get("expires_at") is not None and now >= entry.get("expires_at")
-        ]
+        expired_keys = []
+        for token, entry in data.items():
+            if isinstance(entry, (int, float)):
+                exp = float(entry)
+            elif isinstance(entry, dict):
+                exp = entry.get("expires_at")
+            else:
+                continue
+            if exp is not None and now >= exp:
+                expired_keys.append(token)
         for token in expired_keys:
             del data[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 `@tinyagentos/auth.py` around lines 60 - 67, Update _prune_expired to recognize
legacy float session entries as expiration timestamps in addition to dictionary
entries, and remove them when now is at or past their expiry. Preserve the
existing handling for dictionary entries and leave unrecognized entry shapes
untouched.
tinyagentos/routes/project_notes.py-23-30 (1)

23-30: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add length limits to CreateNoteIn and UpdateNoteIn.

title and body accept any string length. An approved agent holding a project_notes grant can write arbitrarily large rows into project_notes, which grows projects.db without bound. Add max_length constraints so the route rejects oversized payloads with a 422.

🛡️ Proposed fix
-from pydantic import BaseModel
+from pydantic import BaseModel, Field
@@
 class CreateNoteIn(BaseModel):
-    title: str
-    body: str = ""
+    title: str = Field(max_length=200)
+    body: str = Field(default="", max_length=100_000)
 
 
 class UpdateNoteIn(BaseModel):
-    title: str | None = None
-    body: str | None = None
+    title: str | None = Field(default=None, max_length=200)
+    body: str | None = Field(default=None, max_length=100_000)
🤖 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/project_notes.py` around lines 23 - 30, Add max_length
constraints to the title and body fields in CreateNoteIn and UpdateNoteIn so
oversized payloads are rejected with a 422, while preserving their existing
required, default, and optional behavior.
tinyagentos/routes/decisions.py-529-529 (1)

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

Do not mint a device against the "user" sentinel.

If decision["user_id"] is empty, decided_by becomes the literal string "user", and line 552 registers a device owned by a user id that matches no account. That device never appears in any owner's device list, so the owner cannot revoke or block it. Creation guards against a missing admin today, so this path should be unreachable. Fail closed rather than relying on that.

🛡️ Proposed fix
-    decided_by = decision.get("user_id") or "user"
+    decided_by = decision.get("user_id")
+    if not decided_by:
+        logger.warning(
+            "device pairing: decision %s has no user_id; refusing to mint",
+            decision.get("id"),
+        )
+        return True
🤖 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/decisions.py` at line 529, Update the decision handling
around decided_by so an empty decision["user_id"] fails closed instead of
falling back to the literal "user" sentinel. Ensure device creation cannot
proceed without a valid user ID, while preserving normal behavior for decisions
with a present user_id and the existing registration flow.
tests/test_project_notes_store.py-47-53 (1)

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

Newest-first ordering tests can flake on identical created_at values. ProjectNotesStore.list_notes sorts by created_at DESC with no secondary key, and create_note stamps created_at with time.time(). Notes created back-to-back in a test can receive the same float timestamp, and SQLite then returns them in an arbitrary order.

  • tests/test_project_notes_store.py#L47-L53: three notes are created with no delay, then a strict [c, b, a] order is asserted. Either add a tiebreaker to list_notes (for example ORDER BY created_at DESC, id DESC) and keep the assertion, or force distinct timestamps between the create_note calls.
  • tests/test_routes_project_notes.py#L111-L121: the same pattern with two notes and a strict [b, a] assertion. Apply the same fix so both sites depend on a deterministic ordering contract.

A tiebreaker in list_notes is the better fix, because it makes the ordering deterministic for production callers as well, not only for tests.

🤖 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_project_notes_store.py` around lines 47 - 53, The newest-first
ordering is nondeterministic when notes share created_at timestamps. Update
ProjectNotesStore.list_notes to order by created_at descending with id
descending as the secondary key; this root-cause fix applies to
tests/test_project_notes_store.py lines 47-53 and
tests/test_routes_project_notes.py lines 111-121, which require no direct
changes.
tests/test_contacts_peer.py-762-790 (1)

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

Pin the real signing key, otherwise the tamper assertion is vacuous.

The contact is registered with ed25519_pub="pk", which is not the key that signed the envelope. Signature verification therefore fails for the untampered envelope as well. The test passes even if tamper detection is removed, so it proves only that a wrong pinned key returns 403.

test_inbox_nonce_replay at Line 666 shows the correct setup: pin the local identity signing pubkey so a valid envelope succeeds. Then the tampered envelope isolates the tamper path.

🐛 Proposed fix
     async def test_inbox_tampered_signature_rejected(self, client_with_contacts, app_with_contacts):
         """A tampered envelope must be rejected with 403 at the route level."""
         local_id = resolve_local_identity_id()
 
+        # Pin the key that actually signs envelopes, so only the tampering
+        # can be the cause of the rejection.
+        from tinyagentos.hub.identity import public_identity as _hub_pub
+        signing_pub = _hub_pub()["signing_pubkey"]
+
         store = app_with_contacts.state.contacts_store
         await store.add_contact(
             contact_id="hub:tamper-test", hub_username="tamper-test", display_name="T",
-            ed25519_pub="pk", x25519_pub="ek",
+            ed25519_pub=signing_pub, x25519_pub="ek",
         )
🤖 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_contacts_peer.py` around lines 762 - 790, Update
test_inbox_tampered_signature_rejected to register the contact with the actual
local identity Ed25519 signing public key, matching the setup used by
test_inbox_nonce_replay, instead of the placeholder "pk". Keep the envelope
tampering unchanged so the untampered signature is valid and the 403 assertion
specifically verifies tamper detection.
🧹 Nitpick comments (14)
desktop/src/stores/__tests__/reduce-effects.test.ts (1)

31-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test the production data-perf behavior.

This test manually sets data-perf before asserting it. It passes if the useEffect in desktop/src/App.tsx is removed or inverted.

Render the production behavior, or extract the root-attribute update into a shared helper and test that helper.

🤖 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/stores/__tests__/reduce-effects.test.ts` around lines 31 - 45,
Update the reduce-effects test around useThemeStore and the document root so it
exercises the production data-perf synchronization rather than manually setting
or removing the attribute. Render the relevant App behavior, or extract its
root-attribute update logic into a shared helper and test that helper for both
enabled and disabled reduceEffects states.
tinyagentos/routes/device_pair_requests.py (1)

175-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the swallowed notification failure.

The Decision path at line 160 logs its failure, but this handler discards the exception. A broken notification store is then invisible. Log at warning level for parity.

🔧 Proposed fix
-            except Exception:
-                pass
+            except Exception:
+                logger.warning(
+                    "device_pair_requests: bell notification failed", exc_info=True
+                )
🤖 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/device_pair_requests.py` around lines 175 - 176, Update
the exception handler in the device-pair notification flow to log the caught
notification-store failure at warning level before continuing, matching the
logging behavior of the Decision path while preserving the existing
exception-swallowing behavior.

Source: Linters/SAST tools

tinyagentos/routes/project_notes.py (1)

127-135: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

update_note can return null with status 200.

store.update_note returns None when the row no longer exists. A concurrent delete between line 124 and line 127 makes this handler return a JSON null body with status 200. Return the existence-hiding 404 instead, so the client sees a consistent contract.

🔧 Proposed fix
     updated = await store.update_note(
         note_id,
         title=payload.title,
         body=payload.body,
     )
+    if updated is None:
+        return JSONResponse({"error": "not found"}, status_code=404)
🤖 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/project_notes.py` around lines 127 - 135, In the note
update handler, validate the result of store.update_note before logging activity
or returning it. When updated is None, return the existing existence-hiding 404
response; otherwise preserve the current activity logging and successful return
behavior.
tinyagentos/auth.py (2)

52-58: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Write the sessions file atomically.

_save_pruned truncates and rewrites the session file in place. If the process stops during the write, every live session is lost or the file becomes invalid JSON. Startup pruning now rewrites the whole file on every boot, so the exposure grows. Write to a temporary file in the same directory and then os.replace it.

♻️ Proposed refactor
     def _save_pruned(self, data: dict) -> None:
         self._prune_expired(data)
-        self._path.write_text(json.dumps(data))
+        tmp = self._path.with_suffix(self._path.suffix + ".tmp")
+        tmp.write_text(json.dumps(data))
+        os.replace(tmp, self._path)
🤖 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/auth.py` around lines 52 - 58, Update Auth’s _save_pruned method
to serialize the pruned session data to a temporary file in the same directory,
then atomically replace self._path with os.replace. Ensure the temporary file is
safely cleaned up on failure while preserving the existing _prune_expired
behavior.

197-222: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Take the session lock during startup pruning.

_prune_sessions_on_startup calls self._sessions._load() and self._sessions._save(data) outside self._sessions._lock. Every other read-modify-write path in _PersistentSessions holds that lock. The call currently runs in __init__ before requests arrive, so this is defensive only, but a second AuthManager on the same data directory would race. Consider adding a locked helper on _PersistentSessions instead of reaching into the private methods.

🤖 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/auth.py` around lines 197 - 222, Update
_prune_sessions_on_startup to perform the load-and-save pruning operation under
_PersistentSessions._lock, preferably by adding a dedicated locked helper on
_PersistentSessions and calling that helper instead of accessing _load() and
_save() directly. Preserve the existing removed-count calculation and logging
behavior.
tinyagentos/device_pair_requests_store.py (1)

72-84: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

_is_expired fails open on an unparsable timestamp.

If expires_at_ts cannot be parsed, the function returns False, so the request stays live forever. Return True for a corrupt value so a damaged row expires instead of remaining approvable.

🛡️ Proposed fix
     try:
         exp = datetime.fromisoformat(expires_at_ts)
     except ValueError:
-        return False
+        # A corrupt timestamp must not leave the request pending forever.
+        return True
🤖 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/device_pair_requests_store.py` around lines 72 - 84, Update
_is_expired so a non-empty expires_at_ts that raises ValueError in
datetime.fromisoformat is treated as expired by returning True; preserve the
existing False behavior for missing expiry values and normal timestamp
comparisons.
tinyagentos/projects/notes_store.py (1)

56-63: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider pagination for list_notes.

list_notes returns every note for a project in one response. Notes accumulate over a project's life. Add limit and offset parameters with a default cap so the route response stays bounded.

♻️ Proposed signature change
-    async def list_notes(self, project_id: str) -> list[dict]:
+    async def list_notes(
+        self, project_id: str, limit: int = 200, offset: int = 0
+    ) -> list[dict]:
         async with self._db.execute(
-            "SELECT * FROM project_notes WHERE project_id = ? ORDER BY created_at DESC",
-            (project_id,),
+            "SELECT * FROM project_notes WHERE project_id = ? "
+            "ORDER BY created_at DESC LIMIT ? OFFSET ?",
+            (project_id, limit, offset),
         ) as cur:
🤖 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/notes_store.py` around lines 56 - 63, Update list_notes
to accept limit and offset parameters, applying a sensible default maximum limit
and the offset in its SQL query so responses remain bounded while preserving the
existing project_id filter and newest-first ordering. Ensure callers can
override the limit when needed.
tinyagentos/routes/decisions.py (1)

565-569: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

A failure inside the applier leaves the pair request pending.

If device_store.register or set_decision raises, the Decision is already answered but the pair request keeps status = 'pending'. No new Decision is raised for it. The polling device only recovers when the TTL passes and _live_status reports expired, which is up to _TTL_SECS. Consider transitioning the request to denied in the exception handler so the device learns the outcome immediately.

🤖 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/decisions.py` around lines 565 - 569, Update the exception
handler in the device-pairing grant applier to transition the associated pair
request to denied when device_store.register or set_decision fails, before
logging and returning. Preserve the existing warning and ensure the failure does
not leave the request pending.
tests/test_contacts_peer.py (1)

792-820: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confirm the freshness check runs before signature verification.

This test also pins ed25519_pub="pk", so the signature can never verify. It expects 400 rather than 403, which holds only while the route checks envelope freshness before the signature. If that order changes, the test starts asserting the wrong status for the wrong reason.

Pinning the real signing pubkey, as in test_inbox_nonce_replay at Line 666, would make the 400 depend only on the future timestamp.

🤖 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_contacts_peer.py` around lines 792 - 820, Update
test_inbox_future_timestamp_rejected to use the valid signing public key setup
from test_inbox_nonce_replay instead of the placeholder ed25519_pub="pk", so
signature verification can succeed and the expected 400 specifically confirms
freshness validation occurs before signature verification.
tests/test_auth.py (2)

139-152: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename this test or make it construct a new AuthManager.

The name states "at startup", but the body never constructs a second AuthManager. It calls cleanup_sessions() on the existing instance, which duplicates test_cleanup_sessions at Line 129. Startup pruning is exercised by _prune_sessions_on_startup, which only runs in AuthManager.__init__.

♻️ Proposed change to exercise startup pruning
-    def test_prune_expired_sessions_at_startup(self, tmp_path):
-        """Expired entries are pruned when AuthManager is initialized."""
-        mgr = AuthManager(tmp_path)
-        # Create an expired session (will be written to file)
-        token = mgr.create_session(user_id="uid1")
-        # Manually expire it and force prune
-        mgr._sessions[token] = {"user_id": "uid1", "expires_at": time.time() - 1, "long_lived": False}
-        mgr.cleanup_sessions()
-        assert token not in mgr._sessions
-        
-        # Create a valid session
-        token2 = mgr.create_session(user_id="uid2")
-        # Verify it's still valid
-        assert mgr.validate_session(token2) is not None
+    def test_prune_expired_sessions_at_startup(self, tmp_path):
+        """Expired entries are pruned when AuthManager is initialized."""
+        mgr = AuthManager(tmp_path)
+        expired = mgr.create_session(user_id="uid1")
+        live = mgr.create_session(user_id="uid2")
+        mgr._sessions[expired] = {
+            "user_id": "uid1", "expires_at": time.time() - 1, "long_lived": False
+        }
+
+        # A fresh manager over the same data dir must prune at construction.
+        reloaded = AuthManager(tmp_path)
+        assert expired not in reloaded._sessions
+        assert reloaded.validate_session(live) is not None
🤖 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 139 - 152, Update
test_prune_expired_sessions_at_startup to persist an expired session, then
construct a new AuthManager instance so _prune_sessions_on_startup runs during
AuthManager.__init__; assert the expired token is removed and a valid session
remains usable, without calling cleanup_sessions on the original manager.

209-219: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the no-op property, or drop this test.

The body is identical to test_prune_at_startup_preserves_unexpired at Line 166 except for the token string. It asserts preservation, not that pruning is a no-op. Compare the file content before and after construction to test the stated behavior.

♻️ Proposed change
     def test_prune_already_clean_is_noop(self, tmp_path):
         """Pruning an already-clean store (no expired entries) is a no-op."""
-        # Create a file with no expired entries (simulating clean state)
         token1 = "valid_token_789"
         session_data = {token1: {"user_id": "uid1", "expires_at": time.time() + 3600, "long_lived": False}}
         sessions_file = tmp_path / ".auth_sessions"
         sessions_file.write_text(json.dumps(session_data))
-        
-        # Create new manager - should preserve the valid session
-        mgr2 = AuthManager(tmp_path)
-        assert token1 in mgr2._sessions
+
+        AuthManager(tmp_path)
+
+        # Nothing was removed: the persisted entry set is unchanged.
+        assert json.loads(sessions_file.read_text()) == session_data
🤖 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 209 - 219, Update
test_prune_already_clean_is_noop to capture the sessions file contents before
constructing AuthManager, then compare them with the file contents afterward to
assert pruning is a true no-op; retain the existing valid-session setup and
remove the redundant in-memory membership-only assertion.
tests/test_routes_agent_auth_requests.py (1)

1385-1414: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated store setup into a fixture, and release stores in finally.

All four tests in TestDeferBindingApproval repeat the same block: construct AgentRegistryStore, AuthRequestsStore, AgentGrantsStore, and ProjectStore, init each, load the keypair, then monkeypatch the same app.state attributes.

The stores are also closed by plain await calls at the end of each test. If an assertion fails, those calls never run and the aiosqlite connections stay open for the rest of the session. A fixture with teardown fixes both problems.

♻️ Proposed fixture
`@pytest_asyncio.fixture`
async def defer_env(client, monkeypatch, tmp_path):
    """Real stores wired into app.state for the defer_binding approval tests."""
    from tinyagentos.agent_registry_store import (
        AgentRegistryStore,
        load_or_create_signing_keypair,
    )
    from tinyagentos.auth_requests_store import AuthRequestsStore
    from tinyagentos.agent_grants_store import AgentGrantsStore
    from tinyagentos.projects.project_store import ProjectStore

    registry = AgentRegistryStore(tmp_path / "reg.db")
    auth_store = AuthRequestsStore(tmp_path / "auth.db")
    grants = AgentGrantsStore(tmp_path / "grants.db")
    pstore = ProjectStore(tmp_path / "projects.db")
    stores = (registry, auth_store, grants, pstore)
    for s in stores:
        await s.init()
    priv, pub = load_or_create_signing_keypair(tmp_path / "keys")

    state = client._transport.app.state
    monkeypatch.setattr(state, "agent_registry", registry)
    monkeypatch.setattr(state, "auth_requests", auth_store)
    monkeypatch.setattr(state, "agent_grants", grants)
    monkeypatch.setattr(state, "project_store", pstore)
    monkeypatch.setattr(state, "agent_registry_keypair", (priv, pub))

    try:
        yield SimpleNamespace(
            client=client, registry=registry, auth_store=auth_store,
            grants=grants, pstore=pstore, pub=pub,
        )
    finally:
        for s in stores:
            await s.close()

Each test then drops its setup and teardown blocks and uses defer_env.

🤖 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_agent_auth_requests.py` around lines 1385 - 1414, Extract
the repeated store initialization, keypair setup, and app.state monkeypatching
from all four tests in TestDeferBindingApproval into a shared async defer_env
fixture. Have the fixture yield the client, stores, and public key, then close
every initialized store in a finally block so cleanup runs after assertion
failures. Update each test to use defer_env and remove its duplicated setup and
direct teardown calls.
tests/test_project_notes_store.py (1)

120-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The test name claims migration over an existing database, but the body creates a fresh one.

ProjectNotesStore(db) at Line 126 runs against an empty path, so no pre-existing schema is migrated. The docstring already concedes this ("on a fresh install"), which contradicts the name. As written, the test cannot fail if a retrofit path for older databases is removed.

tests/test_device_store.py Lines 108-149 shows the pattern that does test a retrofit: build the old table by hand, then init the store.

Either rename this test to test_index_created_on_fresh_db, or build a pre-existing project_notes table without the index first. Also close the store in finally, because the assert at Line 133 currently runs before await s.close().

♻️ Proposed minimal fix
 `@pytest.mark.asyncio`
-async def test_migration_creates_index_on_existing_db(tmp_path):
+async def test_index_created_on_fresh_db(tmp_path):
     """A notes table on a fresh install carries the created_at DESC index that
     list_notes relies on for ordering."""
     import sqlite3
 
     db = tmp_path / "projects.db"
     s = ProjectNotesStore(db)
     await s.init()
-    conn = sqlite3.connect(str(db))
-    idxs = [r[0] for r in conn.execute(
-        "SELECT name, tbl_name FROM sqlite_master WHERE type='index' AND tbl_name='project_notes'"
-    )]
-    conn.close()
-    assert "idx_notes_project_created" in idxs
-    await s.close()
+    try:
+        conn = sqlite3.connect(str(db))
+        try:
+            idxs = [r[0] for r in conn.execute(
+                "SELECT name FROM sqlite_master "
+                "WHERE type='index' AND tbl_name='project_notes'"
+            )]
+        finally:
+            conn.close()
+        assert "idx_notes_project_created" in idxs
+    finally:
+        await s.close()
🤖 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_project_notes_store.py` around lines 120 - 134, Correct
test_migration_creates_index_on_existing_db so it genuinely covers an existing
database: create the legacy project_notes table without the index before
ProjectNotesStore.init(), or rename it to test_index_created_on_fresh_db if only
fresh-install behavior is intended. Ensure await s.close() runs in a finally
block so the store is closed even when the assertion fails.
tests/test_routes_project_notes.py (1)

27-41: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Close only the stores this fixture opened.

Setup initializes agent_registry and agent_grants only when store._db is None. Teardown closes them unconditionally. If another fixture already opened a store, this fixture closes a store it does not own, and leaves app.state holding a closed store for later tests.

tests/test_token_rotation.py (Lines 23-35) handles the same two stores and deliberately does not close them for this reason.

Track what setup opened and close only that.

♻️ Proposed fix
 `@pytest_asyncio.fixture`
 async def ctx(client):
     """Reuse the session-admin `client` app and additionally init the agent
     registry + grants stores so tokens can be minted against real stores."""
     app = client._transport.app
+    opened = []
     for attr in ("agent_registry", "agent_grants"):
         store = getattr(app.state, attr)
         if store._db is None:
             await store.init()
+            opened.append(store)
     uid = app.state.auth.find_user("admin")["id"]
-    yield SimpleNamespace(client=client, app=app, uid=uid)
-    for attr in ("agent_registry", "agent_grants"):
-        store = getattr(app.state, attr)
-        if store._db is not None:
-            await store.close()
+    try:
+        yield SimpleNamespace(client=client, app=app, uid=uid)
+    finally:
+        for store in opened:
+            await store.close()
🤖 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_project_notes.py` around lines 27 - 41, Update the ctx
fixture’s setup and teardown to track which stores were initialized by this
fixture. In the loops over agent_registry and agent_grants, record only stores
whose _db was None before calling init, then close only those recorded stores
during teardown; leave pre-existing stores open.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a6d731bb-1f1b-4657-80a0-b3e42518934c

📥 Commits

Reviewing files that changed from the base of the PR and between 6099c5d and fc1de1d.

⛔ 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 (98)
  • .claude/skills/taos-agent/SKILL.md
  • CHANGELOG.md
  • changelog.d/.gitkeep
  • desktop/package.json
  • desktop/src/App.tsx
  • desktop/src/__tests__/entry-guards.test.ts
  • desktop/src/app-standalone-main.tsx
  • desktop/src/apps/ActivityApp.test.tsx
  • desktop/src/apps/LibraryApp.settings.test.tsx
  • desktop/src/apps/LibraryApp.test.tsx
  • desktop/src/apps/LibraryApp.tsx
  • desktop/src/apps/MediaPlayerApp.test.tsx
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/apps/ProjectsApp/ProjectMembers.test.tsx
  • desktop/src/apps/TasksApp.tsx
  • desktop/src/apps/chat/ChannelSidebar.tsx
  • desktop/src/apps/chat/__tests__/ChannelSidebar.test.tsx
  • desktop/src/apps/chat/__tests__/content-blocks.test.tsx
  • desktop/src/apps/chat/__tests__/render-helpers.test.tsx
  • desktop/src/apps/codingstudio/BuildView.test.tsx
  • desktop/src/apps/codingstudio/CodeView.test.tsx
  • desktop/src/chat-main.tsx
  • desktop/src/components/Desktop.tsx
  • desktop/src/components/StatusBlock.tsx
  • desktop/src/components/ToolCallBlock.tsx
  • desktop/src/components/__tests__/ConfirmDialog.test.tsx
  • desktop/src/components/__tests__/StatusBlock.test.tsx
  • desktop/src/components/__tests__/ToolCallBlock.test.tsx
  • desktop/src/lib/agent-browsers.ts
  • desktop/src/lib/framework-api.ts
  • desktop/src/lib/github.test.ts
  • desktop/src/lib/github.ts
  • desktop/src/lib/knowledge.test.ts
  • desktop/src/lib/knowledge.ts
  • desktop/src/lib/library.ts
  • desktop/src/lib/mail.test.ts
  • desktop/src/lib/memory-api.test.ts
  • desktop/src/lib/memory-api.ts
  • desktop/src/lib/memory.test.ts
  • desktop/src/lib/memory.ts
  • desktop/src/lib/models.test.ts
  • desktop/src/lib/projects.test.ts
  • desktop/src/lib/projects.ts
  • desktop/src/lib/reddit.test.ts
  • desktop/src/lib/reddit.ts
  • desktop/src/lib/x-monitor.ts
  • desktop/src/lib/youtube.ts
  • desktop/src/registry/app-registry.ts
  • desktop/src/stores/__tests__/reduce-effects.test.ts
  • desktop/tests/x-monitor.test.ts
  • docs/agent-coordination.md
  • docs/agent-manual/04-apps.md
  • docs/agent-manual/index.md
  • docs/changelog-fragments.md
  • docs/design/plan-desktop-shell-core.md
  • docs/design/remote-desktop-app-audit.md
  • docs/doc-gate.toml
  • docs/gitea.md
  • docs/superpowers/plans/2026-04-12-x-monitor.md
  • docs/superpowers/specs/2026-04-12-app-runtime-design.md
  • docs/superpowers/specs/2026-04-12-github-browser-design.md
  • docs/superpowers/specs/2026-04-12-x-monitor-design.md
  • docs/taos-agent-manual.md
  • pyproject.toml
  • scripts/check_doc_gate.py
  • scripts/collate_changelog.py
  • tests/conftest.py
  • tests/routes/test_device_pair_requests.py
  • tests/routes/test_devices.py
  • tests/test_agent_registry_store.py
  • tests/test_auth.py
  • tests/test_collate_changelog.py
  • tests/test_contacts_peer.py
  • tests/test_device_store.py
  • tests/test_doc_gate.py
  • tests/test_project_notes_store.py
  • tests/test_routes_agent_auth_requests.py
  • tests/test_routes_notifications.py
  • tests/test_routes_project_notes.py
  • tests/test_token_rotation.py
  • tinyagentos/__init__.py
  • tinyagentos/agent_registry_store.py
  • tinyagentos/agent_token_auth.py
  • tinyagentos/app.py
  • tinyagentos/auth.py
  • tinyagentos/auth_middleware.py
  • tinyagentos/device_pair_requests_store.py
  • tinyagentos/device_store.py
  • tinyagentos/projects/ids.py
  • tinyagentos/projects/notes_store.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/agent_auth_requests.py
  • tinyagentos/routes/agent_registry.py
  • tinyagentos/routes/decisions.py
  • tinyagentos/routes/device_pair_requests.py
  • tinyagentos/routes/devices.py
  • tinyagentos/routes/notifications.py
  • tinyagentos/routes/project_notes.py

Comment on lines +88 to +89
Install command (quote exactly):
`curl -fsSL https://raw.githubusercontent.com/jaylfc/tinyagentos/master/scripts/install-server.sh | sudo bash`

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 -euo pipefail

echo "== target file excerpt =="
if [ -f .claude/skills/taos-agent/SKILL.md ]; then
  nl -ba .claude/skills/taos-agent/SKILL.md | sed -n '70,105p'
else
  echo "missing .claude/skills/taos-agent/SKILL.md"
fi

echo
echo "== install-server.sh usages in repo =="
rg -n "install-server\.sh|tinyagentos|master|curl.*sha256|sha256sum|gpg|checksum|tag" .claude README.md 2>/dev/null || true

echo
echo "== git status/diff stat =="
git status --short 2>/dev/null || true
git diff --stat 2>/dev/null || true

Repository: jaylfc/taOS

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Python excerpt for target file =="
python3 - <<'PY'
from pathlib import Path
p = Path(".claude/skills/taos-agent/SKILL.md")
if not p.exists():
    print("missing .claude/skills/taos-agent/SKILL.md")
else:
    lines = p.read_text(encoding="utf-8").splitlines()
    for i in range(max(0, len(lines[:90])-10), min(len(lines), 100)):
        print(f"{i+1:4}: {lines[i]}")
PY

echo
echo "== install-server.sh usages in repo =="
rg -n "install-server\.sh|tinyagentos|master|curl.*sha256|sha256sum|gpg|checksum|tag|wget" .claude README.md 2>/dev/null || true

echo
echo "== git status/diff stat =="
git status --short 2>/dev/null || true
git diff --stat 2>/dev/null || true

Repository: jaylfc/taOS

Length of output: 14577


Pin the controller installer to a release commit and add verification before running it as root.

This installs from jaylfc/tinyagentos master and pipes directly to sudo bash; an attack on that branch or a branch rename can execute arbitrary root code. Use an immutable release artifact or a release commit SHA, include a checksum/sig verification step, avoid the install pipeline for beta releases, and document how root is used.

🧰 Tools
🪛 SkillSpector (2.5.1)

[warning] 89: [PE2] Sudo/Root Execution: Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Remediation: Avoid sudo/root unless strictly required. Prefer least-privilege patterns. If elevation is needed, document the justification and scope.

(Privilege Escalation (PE2))


[error] 89: [TM2] Chaining Abuse: Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.

Remediation: Limit tool chaining depth and validate the output of each tool before passing it to the next. Require explicit user approval for multi-step chains.

(Tool Misuse (TM2))

🤖 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 @.claude/skills/taos-agent/SKILL.md around lines 88 - 89, Update the
controller installation instructions around the quoted curl command to use an
immutable release artifact or pinned release commit instead of master, verify
its checksum or signature before execution, and avoid piping the downloaded
script directly to sudo bash. For beta releases, provide a non-pipeline
download-and-verify flow, and document the specific point and purpose of root
privileges.

Source: Linters/SAST tools

Comment on lines +64 to +66
const headers = new Headers(init?.headers);
headers.set("Accept", "application/json");
const res = await fetch(url, { ...init, headers });

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

Restore CSRF wrapping in the JSON request helpers.

The Headers rewrite sends a plain RequestInit directly to fetch. The known mutating callers therefore omit X-CSRF-Token. Apply withCsrf to the final same-origin request, or wrap each mutating caller.

  • desktop/src/lib/knowledge.ts#L64-L66: wrap the final init; postJson currently creates an unwrapped POST request.
  • desktop/src/lib/github.ts#L82-L84: wrap the final init; the POST caller currently passes unwrapped options.
Proposed fix
-  const res = await fetch(url, { ...init, headers });
+  const res = await fetch(url, withCsrf({ ...init, headers }));

Add the withCsrf import where it is absent. Restrict this approach to same-origin API URLs.

📍 Affects 2 files
  • desktop/src/lib/knowledge.ts#L64-L66 (this comment)
  • desktop/src/lib/github.ts#L82-L84
🤖 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/lib/knowledge.ts` around lines 64 - 66, Restore CSRF wrapping for
the final same-origin JSON request in postJson at
desktop/src/lib/knowledge.ts:64-66 by applying withCsrf to the merged init
before fetch, adding its import if needed. Apply the same final-init wrapping in
the POST caller at desktop/src/lib/github.ts:82-84, ensuring both mutating
requests include X-CSRF-Token without changing handling for non-mutating or
external URLs.

Comment thread scripts/check_doc_gate.py
Comment on lines +96 to +121
def _validate_config(config: dict) -> None:
"""Validate the structural shape of a parsed doc-gate config.

A config that parses as valid TOML but has the wrong shape (e.g.
``rules = "not a list"``) is a config error, not a runtime crash: surface
it as EXIT_CONFIG_ERROR rather than letting it die in the rule loop with
an AttributeError."""
if not isinstance(config, dict):
raise ValueError("config root must be a table")
rules = config.get("rules", [])
if not isinstance(rules, list):
raise ValueError("'rules' must be a list of tables")
for i, rule in enumerate(rules):
if not isinstance(rule, dict):
raise ValueError(f"rules[{i}] must be a table")
gate = config.get("gate", {})
if not isinstance(gate, dict):
raise ValueError("'gate' must be a table")
if "trailer" in gate and not isinstance(gate["trailer"], str):
raise ValueError("'gate.trailer' must be a string")
invariants = config.get("invariants", {})
if not isinstance(invariants, dict):
raise ValueError("'invariants' must be a table")
scan = invariants.get("referenced_paths_scan", [])
if not isinstance(scan, list):
raise ValueError("'invariants.referenced_paths_scan' must be a list")

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

Validate rule and invariant member types.

_validate_config accepts a rule such as when_changed = 1 because it only checks that the rule is a table. That invalid value then reaches rule evaluation instead of returning EXIT_CONFIG_ERROR.

Validate name, when_changed, require_doc, hint, and on_modify. Also require string entries in referenced_paths_scan.

🤖 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_doc_gate.py` around lines 96 - 121, Extend _validate_config to
validate each rule member: require name, when_changed, require_doc, hint, and
on_modify to be strings when present, rejecting invalid values with ValueError
so callers return EXIT_CONFIG_ERROR. Also iterate over
invariants.referenced_paths_scan and require every entry to be a string,
preserving existing structural checks and defaults.

Comment on lines +105 to +116
text = CHANGELOG.read_text(encoding="utf-8")
if UNRELEASED not in text:
print(f"collate-changelog: {CHANGELOG.name} has no '{UNRELEASED}' anchor", file=sys.stderr)
return 1
# Insert directly BELOW [Unreleased] so Unreleased stays empty and on top,
# which is what the release train expects on the next cycle.
anchor = UNRELEASED + "\n"
text = text.replace(anchor, anchor + "\n" + section, 1)
CHANGELOG.write_text(text, encoding="utf-8")

for path in consumed:
path.unlink()

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 | ⚡ Quick win

Make insertion and fragment cleanup safe to retry.

If CHANGELOG.md ends directly after ## [Unreleased], Line 106 passes but Line 112 cannot match the newline-suffixed anchor. The script then deletes every fragment without inserting a release section.

Confirm that exactly one anchor insertion occurred before cleanup. Also detect an existing version section so a retry after interruption between the write and unlink() calls cannot add the release twice.

🤖 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/collate_changelog.py` around lines 105 - 116, Make the insertion
logic in the changelog collation flow robust when [Unreleased] has no trailing
newline, and verify exactly one insertion occurred before deleting consumed
fragments. Before inserting, detect whether the target version section already
exists and skip insertion when it does, so retries cannot duplicate releases;
only perform fragment cleanup after confirming the changelog was successfully
updated or already contains that version section.

Comment on lines +115 to +119
# Reject tokens issued before the identity's token_min_iat cutoff (rotation).
token_min_iat = record.get("token_min_iat") or 0
token_iat = payload.get("iat") or 0
if token_iat < token_min_iat:
raise HTTPException(status_code=401, detail="token superseded")

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

Apply token rotation to check_agent_identity.

This cutoff check only protects _verify_agent_scope. check_agent_identity validates the same token and active registry record but does not check token_min_iat. A superseded token can still pass identity-gated flows.

Extract the active-token and cutoff validation into a shared helper, then call it from both paths. Add a regression test for a rotated token through check_agent_identity.

🤖 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_token_auth.py` around lines 115 - 119, Extract the
active-token and token_min_iat cutoff validation from _verify_agent_scope into a
shared helper, preserving the existing 401 behavior for superseded tokens.
Invoke this helper from both _verify_agent_scope and check_agent_identity so
rotated tokens are rejected in identity-gated flows, and add a regression test
covering a superseded token passed to check_agent_identity.

Comment on lines +85 to +89
# Project notes: read/write a project's persistent idea notes
# (title + markdown body) by a session owner/admin or a project-bound
# project_notes grant holder. Mirrors project_doc_review's single-scope
# read+write surface for a lightweight persistent notes surface.
"project_notes",

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 | 🔴 Critical | ⚡ Quick win

Bind project_notes through _PROJECT_SCOPES.

project_notes is project-bound, but _PROJECT_SCOPES does not include it. The required explicit project_id check then does not run. A non-deferred approval can use the unauthenticated request record’s project_id as effective_project and issue a grant for a project that the operator did not select.

Add "project_notes" to _PROJECT_SCOPES before release.

Proposed fix
-_PROJECT_SCOPES = {"project_tasks", "project_tasks_create", "project_tasks_update"} | _CANVAS_SCOPES | _FILES_SCOPES
+_PROJECT_SCOPES = {
+    "project_tasks",
+    "project_tasks_create",
+    "project_tasks_update",
+    "project_notes",
+} | _CANVAS_SCOPES | _FILES_SCOPES
🤖 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 85 - 89, Add
"project_notes" to the _PROJECT_SCOPES collection so project_notes requests
undergo the required explicit project_id validation and cannot derive
effective_project from an unauthenticated request record when issuing grants.

Comment on lines +783 to +785
ts = int(time.time())
before_iat = record.get("token_min_iat") or 0
updated = await store.bump_token_min_iat(canonical_id, ts)

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 | 🏗️ Heavy lift

Use a rotation value with finer resolution than integer-second iat.

A token minted before rotation in the same Unix second has iat == token_min_iat. The < check accepts it, so rotation does not invalidate every token issued before the request. The current test encodes this ambiguous same-second behavior.

  • tinyagentos/routes/agent_registry.py#L783-L785: use a signed monotonic token version or a higher-resolution issued-at value for the cutoff.
  • tests/test_token_rotation.py#L166-L174: control the clock and assert that a token issued before rotation in the same second is rejected while a post-rotation token is accepted.
📍 Affects 2 files
  • tinyagentos/routes/agent_registry.py#L783-L785 (this comment)
  • tests/test_token_rotation.py#L166-L174
🤖 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_registry.py` around lines 783 - 785, Update
tinyagentos/routes/agent_registry.py:783-785 in the token-rotation flow around
bump_token_min_iat to use a signed monotonic token version or higher-resolution
issued-at cutoff, rather than integer-second time.time(), so tokens minted
before rotation in the same second are rejected. Update
tests/test_token_rotation.py:166-174 to control the clock and verify that a
pre-rotation same-second token is rejected while a post-rotation token is
accepted.

Comment on lines +137 to +179
admin_id = _admin_user_id(request)
decision_store = getattr(request.app.state, "decision_store", None)
if decision_store is not None and admin_id:
try:
await decision_store.create(
from_agent="@taOSc",
question=(
f"taOSc on {display!r} wants to connect. Code {verify_code}. Allow?"
),
type="approve_deny",
priority="blocking",
project_id=None,
user_id=admin_id,
metadata={
"kind": "device_pairing",
"pair_request_id": pair_request_id,
},
context=(
f"Pairing request from {body.platform} device "
f"at {requester_ip or 'unknown IP'}"
),
)
except Exception:
logger.warning("device_pair_requests: could not raise Decision", exc_info=True)

# Best-effort bell notification so the request leaves an auditable
# trail in the inbox. Must not fail the created request.
notifs = getattr(request.app.state, "notifications", None)
if notifs is not None:
try:
await notifs.add(
title="Pair request",
message=f"{display} wants to connect (code {verify_code})",
level="warning",
source="pair_requests",
user_id=admin_id,
data={"request_id": pair_request_id, "platform": body.platform},
)
except Exception:
pass

# F3 / criterion 5: verify_code is returned ONLY here -- never on the poll.
return {"pair_request_id": pair_request_id, "verify_code": verify_code}

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

A pairing request that cannot raise a Decision still returns 200 and consumes a cap slot.

If decision_store is None or _admin_user_id returns "", the whole approval block is skipped. The route still returns {pair_request_id, verify_code}. The device then polls a request that no human can ever see or approve, and the row holds a pending slot for the full _TTL_SECS window. Repeated calls exhaust _PENDING_CAP for every other device.

Fail the request instead, so the device reports a clear error rather than polling forever.

🔧 Proposed fix
     admin_id = _admin_user_id(request)
     decision_store = getattr(request.app.state, "decision_store", None)
-    if decision_store is not None and admin_id:
+    if decision_store is None or not admin_id:
+        raise HTTPException(
+            status_code=503,
+            detail="pairing is unavailable: no instance admin to approve the request",
+        )
+    if True:
         try:

Place this check before store.create(...) so no row is written when pairing cannot proceed.

📝 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
admin_id = _admin_user_id(request)
decision_store = getattr(request.app.state, "decision_store", None)
if decision_store is not None and admin_id:
try:
await decision_store.create(
from_agent="@taOSc",
question=(
f"taOSc on {display!r} wants to connect. Code {verify_code}. Allow?"
),
type="approve_deny",
priority="blocking",
project_id=None,
user_id=admin_id,
metadata={
"kind": "device_pairing",
"pair_request_id": pair_request_id,
},
context=(
f"Pairing request from {body.platform} device "
f"at {requester_ip or 'unknown IP'}"
),
)
except Exception:
logger.warning("device_pair_requests: could not raise Decision", exc_info=True)
# Best-effort bell notification so the request leaves an auditable
# trail in the inbox. Must not fail the created request.
notifs = getattr(request.app.state, "notifications", None)
if notifs is not None:
try:
await notifs.add(
title="Pair request",
message=f"{display} wants to connect (code {verify_code})",
level="warning",
source="pair_requests",
user_id=admin_id,
data={"request_id": pair_request_id, "platform": body.platform},
)
except Exception:
pass
# F3 / criterion 5: verify_code is returned ONLY here -- never on the poll.
return {"pair_request_id": pair_request_id, "verify_code": verify_code}
admin_id = _admin_user_id(request)
decision_store = getattr(request.app.state, "decision_store", None)
if decision_store is None or not admin_id:
raise HTTPException(
status_code=503,
detail="pairing is unavailable: no instance admin to approve the request",
)
if True:
try:
await decision_store.create(
from_agent="`@taOSc`",
question=(
f"taOSc on {display!r} wants to connect. Code {verify_code}. Allow?"
),
type="approve_deny",
priority="blocking",
project_id=None,
user_id=admin_id,
metadata={
"kind": "device_pairing",
"pair_request_id": pair_request_id,
},
context=(
f"Pairing request from {body.platform} device "
f"at {requester_ip or 'unknown IP'}"
),
)
except Exception:
logger.warning("device_pair_requests: could not raise Decision", exc_info=True)
# Best-effort bell notification so the request leaves an auditable
# trail in the inbox. Must not fail the created request.
notifs = getattr(request.app.state, "notifications", None)
if notifs is not None:
try:
await notifs.add(
title="Pair request",
message=f"{display} wants to connect (code {verify_code})",
level="warning",
source="pair_requests",
user_id=admin_id,
data={"request_id": pair_request_id, "platform": body.platform},
)
except Exception:
pass
# F3 / criterion 5: verify_code is returned ONLY here -- never on the poll.
return {"pair_request_id": pair_request_id, "verify_code": verify_code}
🧰 Tools
🪛 Ruff (0.16.1)

[error] 175-176: try-except-pass detected, consider logging the exception

(S110)


[warning] 175-175: Do not catch blind exception: Exception

(BLE001)

🤖 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/device_pair_requests.py` around lines 137 - 179, Require
an available decision_store and non-empty admin_id before creating the pairing
request or calling decision_store.create in the device-pairing route. If either
prerequisite is missing, fail the request with a clear client-facing error
instead of returning pair_request_id and verify_code; preserve the existing
best-effort notification and success response only when approval can proceed.

Comment thread tests/conftest.py
await feedback_store.close()
await client_log_store.close()
await project_element_store.close()
await project_notes_store.close()

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: Missing device_pair_requests.close() in test fixture teardown

device_pair_requests is initialized in the client fixture setup (line 433) but never closed in the teardown. This leaks a SQLite connection across every test run that uses the client fixture. Compare the symmetric project_notes_store close that was just added at this same location.


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

logger = logging.getLogger(__name__)

# F5: the store does not validate the platform, so the whitelist lives here.
_VALID_PLATFORMS = frozenset({"ios", "watchos", "android"})

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: Platform whitelist mismatch with devices route

_VALID_PLATFORMS includes "android", but routes/devices.py RegisterIn.platform_supported only accepts "ios" and "watchos". A pair request for an Android device will succeed, and upon admin approval _apply_device_pairing_grant will mint a device with platform="android" that the normal /api/devices/register route would reject.


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

class CreateNotificationRequest(BaseModel):
title: str
message: str
level: str = "info"

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: level accepts any string without validation

CreateNotificationRequest.level accepts any string, but the UI HTML renderer at line 40 only knows how to render "info", "warning", and "error". Invalid levels are persisted to the database and silently render with no icon in the notifications list.


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

user_id=admin_id,
data={"request_id": pair_request_id, "platform": body.platform},
)
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: Bare except Exception: pass swallows all errors

The best-effort notification delivery at lines 175-176 catches and discards every exception type, including TypeError, AttributeError, or database failures. This masks real bugs in the auditable notification trail that the comment explicitly says "must not fail the created request". Consider at minimum logging the exception.


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

raise RuntimeError("DevicePairRequestsStore not initialised")
now_iso = _iso(_now())
cur = await self._db.execute(
"SELECT * FROM device_pair_requests "

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: list_pending uses SELECT * while get() uses _SAFE_COLS

This inconsistency means any future schema change adding a sensitive column to device_pair_requests would be silently exposed through list_pending but not through get(). Align list_pending with the explicit _SAFE_COLS projection used by get().


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

{ kind: "thinking", text: "thinking...", collapsed: true },
{ kind: "tool_call", call_id: "c1", name: "Bash", status: "running" as const },
{ kind: "thinking", text: "pondering" },
{ kind: "tool_call", name: "grep", args: {} },

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: Incomplete tool_call block omits required fields

The test data { kind: "tool_call", name: "grep", args: {} } is missing required fields call_id and status. The runtime cast bypasses type checking, so the block renders with a missing status indicator. The test does not assert on the rendered tool_call output, so it does not verify correct rendering.


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

}

function StatusIndicator({ status }: { status: ToolCallStatus }) {
switch (status) {

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: StatusIndicator switch has no default case

If status is not "running", "done", or "error" at runtime (backend change or type mismatch), the component returns undefined and React renders nothing. TypeScript exhaustiveness checking should catch this at compile time, but a runtime mismatch would silently render nothing.


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

const { container } = render(<ToolCallBlock block={blockFor({ status: "error" })} />);
const card = container.querySelector('[data-tool-call="true"]');
expect(card?.getAttribute("data-status")).toBe("error");
expect(card?.querySelector(".lucide-triangle-alert")).not.toBeNull();

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: Wrong lucide-react class name in test assertion

lucide-react generates the class name lucide-alert-triangle (kebab-case of the icon name alert-triangle), not lucide-triangle-alert. This assertion will fail at runtime.

Suggested change
expect(card?.querySelector(".lucide-triangle-alert")).not.toBeNull();
expect(card?.querySelector(".lucide-alert-triangle")).not.toBeNull();

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

// without it, cookie-authenticated project create/update/delete 403 with
// "CSRF token missing". Non-mutating GETs pass through unchanged.
const headers = new Headers(init?.headers);
headers.set("Content-Type", "application/json");

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: headers.set("Content-Type", "application/json") unconditionally overwrites caller-provided headers

The original code spread ...(init?.headers || {}) into the headers object, allowing caller headers to override the default Content-Type. The new code always overwrites any Content-Type in init.headers. While correct for JSON callers, it changes behavior for any caller that intentionally sets a different Content-Type (e.g., multipart/form-data).


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

- **Notes** (`notes`), **Todo** (`todo`): shared notes and lists you belong to.
- **Images** (`images`): generate and manage artwork.
- **Browser** (`browser`), **Terminal** (`terminal`): web browsing and shell access.
- Other bundled apps (Library, Channels, Secrets, Tasks, MCP, Guides and more); if you do not know one, guess from its name and point the user to Guides.

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: Skill references Tasks instead of Routines

This PR renames the Tasks app to Routines across all other documentation and the app itself, but this skill file still says Tasks. An OS-native agent reading this skill would try to open a non-existent Tasks app instead of Routines.


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

@kilo-code-bot

kilo-code-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 15 Issues Found | Recommendation: Address before merge

Overview

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

CRITICAL

File Line Issue
tests/conftest.py 511 device_pair_requests initialized but never closed in test fixture teardown, leaking a SQLite connection
desktop/src/components/__tests__/ToolCallBlock.test.tsx 46 Wrong lucide-react class name: .lucide-triangle-alert should be .lucide-alert-triangle

WARNING

File Line Issue
tinyagentos/routes/device_pair_requests.py 46 _VALID_PLATFORMS includes android but devices route only accepts ios/watchos
tinyagentos/routes/notifications.py 54 level accepts any string without validating against allowed values
tinyagentos/routes/device_pair_requests.py 175 Bare except Exception: pass swallows all errors in notification delivery
tinyagentos/device_pair_requests_store.py 243 list_pending uses SELECT * while get() uses _SAFE_COLS
desktop/src/apps/LibraryApp.tsx 1230 parseInt without radix collapses non-numeric input to 0
desktop/src/apps/LibraryApp.tsx 149 ?? does not catch wrong-type values from localStorage
desktop/src/apps/LibraryApp.tsx 1257 React key embeds array index, causing reconciliation issues
desktop/src/apps/chat/__tests__/render-helpers.test.tsx 107 Incomplete tool_call block omits required fields call_id and status
desktop/src/components/ToolCallBlock.tsx 62 StatusIndicator switch has no default case
.claude/skills/taos-agent/SKILL.md 113 Skill references Tasks instead of Routines

SUGGESTION

File Line Issue
desktop/src/apps/chat/ChannelSidebar.tsx 274 thinkingChannelIds.includes(ch.id) is O(n) per channel
desktop/src/apps/chat/ChannelSidebar.tsx 501 Same O(n) includes() lookup repeated in desktop channel list
desktop/src/lib/projects.ts 163 headers.set() unconditionally overwrites caller-provided Content-Type
Files Reviewed (11 files)
  • tests/conftest.py - 1 issue
  • tinyagentos/routes/device_pair_requests.py - 2 issues
  • tinyagentos/routes/notifications.py - 1 issue
  • tinyagentos/device_pair_requests_store.py - 1 issue
  • desktop/src/apps/LibraryApp.tsx - 3 issues
  • desktop/src/apps/chat/ChannelSidebar.tsx - 2 issues
  • desktop/src/apps/chat/__tests__/render-helpers.test.tsx - 1 issue
  • desktop/src/components/ToolCallBlock.tsx - 1 issue
  • desktop/src/components/__tests__/ToolCallBlock.test.tsx - 1 issue
  • desktop/src/lib/projects.ts - 1 issue
  • .claude/skills/taos-agent/SKILL.md - 1 issue

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 199.6K · Output: 29.7K · Cached: 2.3M

@jaylfc

jaylfc commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Merging directly rather than via gate_merge.sh: the gate requires a cla check, but cla.yml scopes itself to PRs targeting dev only — its own header says the internal dev→master promotion is deliberately not CLA-gated, so the check is structurally absent here, not missing. All other required checks verified complete and green (8 shards, lint, spa-build, gates).

@jaylfc
jaylfc merged commit 86fa58b into master Aug 9, 2026
33 checks passed
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