Skip to content

Promote dev to master#979

Merged
jaylfc merged 47 commits into
masterfrom
dev
Jun 17, 2026
Merged

Promote dev to master#979
jaylfc merged 47 commits into
masterfrom
dev

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Promotes 40 commits of dev to master. Highlights: userspace app system surfaced in the Launchpad (#974) + per-app updates; the multi-agent build collaboration tooling; chat/projects fixes shipped via the autonomous build team (channel settings overlap #975, channel topic #976, channel-create listing #977, external/connected agents members section #978); README Discord community badge; dependabot Python deps tracked via the uv ecosystem. All landed on dev gated by CI + CodeRabbit + Gitar + Kilo.

Summary by CodeRabbit

  • New Features

    • Added five new studio applications: App Studio, Coding Studio, Design Studio, Music Studio, and Office Suite with customizable views and content creation tools
    • Introduced userspace app platform enabling installation and management of community and first-party applications
    • Added optional app updates tracking in Store's Updates tab
    • Added Studios section to Store showcasing featured and community apps
    • Added Discord community link to README
  • Bug Fixes

    • Fixed channel list refresh in Messages app
    • Improved channel grouping for project-scoped views
    • Corrected positioning of settings panel
    • Documentation-only updates no longer trigger notifications
  • Documentation

    • Updated README with app status and community links
    • Updated project status documentation

jaylfc added 30 commits June 16, 2026 19:27
- Add "studios" to NavId union and NAV array (after "apps", Sparkles icon)
- Add studioState field to CatalogApp type for installed/available/soon lifecycle
- Add STUDIOS_APPS catalog entries (type "studio") with cover gradients
- Add NAV_TYPE_MAP entry for "studios" -> ["studio"]
- Wire StudiosView into the content switcher (alongside discover/community)
- New StudiosView component: hero card (Coding Studio), 4x2 taOS Studios grid
  with Soon badges, community studios horizontal scroll, layout chips row
- StudiosView.test.tsx: 6 tests covering headings, cards, Soon badges, hero
Adds CodingStudioApp as an optional platform app with three views:
BuildView (file tree + syntax editor + build log panel),
TemplatesView (hero prompt + 8 template cards), and PreviewView
(URL bar + device toggle + simulated todo app + dev console).
Registered in app-registry at launchpadOrder 13.25, optional: true.
Includes 6 vitest tests covering titlebar, rail items, and view switching.
First-pass static UI: Studio (transport bar, track list, timeline with
clip blocks, right inspector, piano roll), Compose (AI prompt + style
chips + results list), Sounds (filter pills + 4-col instrument grid).
Matches music-studio-mock.html layout and follows CodingStudioApp shell
pattern (46px titlebar, 68px rail, shell tokens, muted track palette).
9/9 vitest tests pass, tsc clean, vite build succeeds.
Canvas-editor shell matching the approved mock: Design/Templates/Magic
views, left element rail, artboard with selection box, properties panel
with Magic edits chip bar, 8-card template grid, and AI prompt tile.
Static first-pass UI matching the approved office-suite-mock.html design.
Follows CodingStudioApp shell pattern: 46px titlebar, 68px icon rail,
per-view subfolder components, shell tokens throughout.
…es/Publish views

Static first-pass UI matching the approved mock. Follows the canonical
studio shell pattern (46px titlebar, 68px icon rail, per-view subdir).
No registry wiring -- lead handles that separately.

- AppStudioApp.tsx: shell with Build/Templates/Publish/SDK rail
- appstudio/BuildView.tsx: checkerboard sandbox preview (Chore Quest
  app-in-window), build log steps, capabilities panel, model pill,
  prompt bar
- appstudio/TemplatesView.tsx: hero + 8-card template grid
- appstudio/PublishView.tsx: app identity header, capability toggles
  (Workspace/Notifications/Household), safety note, publish side panel
- AppStudioApp.test.tsx: 5 tests covering titlebar, rail items, default
  Build view, Templates switch, and Publish switch with capability rows
Register Coding/Design/Music/App/Office as optional desktop apps. Extend
the optional-frontend-app allowlist to cover them so /api/apps/optional/
install accepts them. Wire the Store Studios section: Get installs via
the optional-app endpoint and emits APP_OPTIONAL_CHANGED (the launcher
surfaces it at once); Open launches the app via the process store. Flip
the four newly built studios from 'soon' to 'available'; Web Studio stays
'soon'.
feat(studios): Creative Studios -- Store Studios section + 5 studio apps + install wiring
* feat(apps): per-app versioning + Updates UI surface (#89 P1)

* fix(apps): address gitar findings on #89 P1

- Store: hoist the taOS Apps updates section above the grid so optional-app
  updates show even when framework updates also exist (was nested in the
  filtered.length===0 empty-state branch and hidden otherwise)
- name/icon for optional apps now come from the app registry (getApp), so the
  five studios resolve proper names/icons instead of raw ids + generic icon
- _semver_tuple pads to (major,minor,patch) so '1.0' and '1.0.0' compare equal,
  and returns (0,0,0) on parse failure so it never masks a real update
…arXNG; Automation Studio brainstorm (#968) + searx-json-default (#969)
…signing, web-first); Automation Studio spec signed off + deferred
* feat(userspace): re-integrate web app-runtime foundation onto dev (#89 P3a)

* fix(userspace): fold CodeRabbit findings on the app-runtime foundation (#89 P3a)

* fix(userspace): zip-bomb size caps + reject app.files.write to the jail root (#89 P3a)

- package.py: cap declared uncompressed total, per-member size, and member count
  before extraction (zip-bomb defense, gitar Security finding)
- install_app: bound the upload/fetch read to a max size, return 413 if exceeded
- broker.py: app.files.write to the jail root or any existing dir now returns
  invalid_path instead of an uncaught IsADirectoryError (500); path containment
  uses is_relative_to
- tests for the zip-bomb cap and the jail-root write guard
…4 on unknown app (security review)

A caller could previously grant a userspace app any capability regardless of
what its manifest requested. Now the granted set is intersected with
permissions_requested, and unknown apps return 404. Test package updated to
request the caps it exercises.
…b) (#972)

* feat(userspace): trust-aware CSP, capabilities, and theme injection for first-party packages (#89 P3b)

- store.py: add trust column (TEXT DEFAULT 'community') with _post_init
  migration (PRAGMA table_info + ALTER TABLE -- same pattern as
  knowledge_store and agent_registry_store); install() accepts trust kwarg

- routes/userspace_apps.py: public install endpoint always writes
  trust='community' (first-party comes only from internal seed/P2 sig path);
  serve_bundle picks _BUNDLE_CSP_FIRST_PARTY vs _BUNDLE_CSP by app trust;
  broker route passes full GATED_CAPS set as granted for first-party apps,
  per-grant set for community apps

- SandboxedAppWindow.tsx: accepts trust prop; posts taosTheme tokens into
  the iframe on load and on scheme changes for first-party apps only;
  community apps receive no theme injection

- userspace-apps.ts: UserspaceAppRow gains optional trust field; toAppManifest
  threads it into the component closure for SandboxedAppWindow

- taos-app-sdk.js: adds taos.theme.get() / taos.theme.subscribe(cb) backed
  by the taosTheme postMessage; community apps never receive messages so the
  API simply returns an empty object for them

- tests: 10 new backend tests (store migration, public endpoint enforces
  community, CSP by trust, broker grants for first-party vs community,
  GATED_CAPS unit); 8 new frontend tests (theme injection on/off by trust,
  SDK theme message handling, trust field in toAppManifest)

* test(userspace): community-grant test requests the cap it grants (reconcile with set_permissions security fix)

* fix(userspace): close trust-retention on reinstall + fold CodeRabbit #972 findings

CRITICAL: the install UPSERT did not update trust on conflict, so a public
install (always community) of an existing first-party app id would overwrite the
bundle while keeping first-party privileges. Fixed two ways:
- store.install UPSERT now sets trust=excluded.trust (a community reinstall
  downgrades trust; re-seeding first-party stays first-party)
- the public install endpoint rejects (409) replacing an app already installed
  as first-party, so a trusted studio's bundle cannot be clobbered at all
Also: SDK guards taosTheme against non-object/array payloads; regression tests
for the 409 reject + the UPSERT trust update; SDK array-rejection test.
…4a) (#973)

* feat(userspace): first-party reference .taosapp + boot-seeding (#89 P4a)

Adds tinyagentos/userspace/seed/welcome/ (manifest.yaml + index.html),
a minimal first-party reference app that exercises the SDK theme API
(get + subscribe), a kv round-trip, and a notify call.

Adds tinyagentos/userspace/seed.py exposing seed_bundled_apps, which
builds each seed subdirectory into an in-memory .taosapp zip, validates
it through extract_package, and calls store.install with trust="first-party".
Idempotent: skips apps already installed at the same version; re-seeds on
a version bump. A seeding error is caught and logged -- it does not crash
startup.

Wires seed_bundled_apps into the app.py lifespan immediately after the
userspace store and data store are initialised. The call is wrapped in a
try/except so a seeding failure only emits a warning.

Adds tests/userspace/test_seed.py covering: install as first-party,
idempotency, version-bump re-seed, missing seed_dir is silent, bundle
route returns first-party CSP, and the real bundled taos-welcome app
for all of the above.

* fix(userspace): seed idempotency requires first-party trust + clears stale files (CodeRabbit/gitar #973)

- the skip-if-same-version check now also requires trust=first-party, so a
  community row claiming a seeded id is re-seeded to first-party rather than
  skipped (CodeRabbit Major)
- a re-seed rmtree's the extracted app dir before extracting, so a smaller new
  version cannot inherit stale files from the old one (gitar edge case)
- tests for both
…st+package+seeding all on dev, holding for Jay
jaylfc added 8 commits June 17, 2026 13:47
The controller and worker migrated to uv (pyproject.toml + uv.lock); CI
installs with uv sync --frozen and there is no pip requirements manifest.
dependabot was auto-detecting the uv project independently of the stale
pip entry and opening uv-group PRs against the default branch (master),
since no config entry pinned their target-branch. Converting the pip
entry to a uv ecosystem entry keeps Python dependency PRs on dev.
…aunchpad (#89)

Wires the userspace app runtime into the desktop launcher: installed .taosapp packages (the seeded first-party welcome app plus community apps) now appear in an Apps section and open into sandboxed windows. Registry id namespaced as userspace:<id> to avoid shadowing built-in apps; syncUserspaceApps reconciles by id preserving manifest identity (no remount of open windows) and clears prefetch state on removal; useInstalledUserspaceApps mirrors the services hook and cancels in-flight refreshes. Store-side install/manage UI is a separate follow-up.
…S); first grok dogfood loop closed end to end
)

Refetch the channel list after creation (matching the DM-create flow) and
include project-scoped channels in the Topics/Groups sidebar sections.
…tion (#978)

Fetch active external-selfjoin registry entries and render matching project
members under External / Connected agents with an external tag, while deployed
container agents stay in the main Members list.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@github-actions

Copy link
Copy Markdown

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

See CONTRIBUTING.md for the branch model.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds creative studio desktop apps and store surfaces, introduces userspace app runtime and desktop integration, expands optional app catalog and update displays, updates project and messaging UI behavior, suppresses docs-only update notifications, and includes README, status, and Dependabot changes.

Changes

Creative studios and optional app catalog

Layer / File(s) Summary
Optional app catalog backend
tinyagentos/routes/apps.py, tests/test_apps_installed.py
Adds optional app allowlist version/trust metadata, returns /api/apps/optional/catalog, records installed versions, and tests installed/update_available behavior.
Studio app shells and views
desktop/src/registry/app-registry.ts, desktop/src/apps/*StudioApp.tsx, desktop/src/apps/*StudioApp.test.tsx, desktop/src/apps/appstudio/*, desktop/src/apps/codingstudio/*, desktop/src/apps/designstudio/*, desktop/src/apps/musicstudio/*, desktop/src/apps/officesuite/*, README.md
Adds App, Coding, Design, Music, and Office studio shells, their static view surfaces, registry entries, and UI tests; README now says two studio apps are in beta.
Studios store and optional update UI
desktop/src/apps/StoreApp/StudiosView.tsx, desktop/src/apps/StoreApp/index.tsx, desktop/src/apps/StoreApp/types.ts, desktop/src/apps/StoreApp/*.test.tsx, desktop/src/apps/SettingsApp/UpdatesPanel.tsx, desktop/src/apps/SettingsApp/UpdatesPanel.test.tsx
Adds the Store “Studios” tab and catalog view, shows optional app updates in Store and Settings, and adds tests for studios and optional app update states.

Userspace app runtime and desktop integration

Layer / File(s) Summary
Userspace storage, package, URL guard, and broker
tinyagentos/userspace/store.py, tinyagentos/userspace/data_store.py, tinyagentos/userspace/package.py, tinyagentos/userspace/url_guard.py, tinyagentos/userspace/broker.py, tests/userspace/test_store.py, tests/userspace/test_data_store.py, tests/userspace/test_package.py, tests/userspace/test_url_guard.py, tests/userspace/test_broker.py
Adds userspace app and data stores, package parsing/extraction, public URL validation, capability dispatch, and unit tests for storage, package safety, SSRF checks, and broker behavior.
Userspace routes, lifecycle, SDK, and seeded app
tinyagentos/routes/userspace_apps.py, tinyagentos/routes/__init__.py, tinyagentos/app.py, tinyagentos/userspace/sdk/taos-app-sdk.js, tinyagentos/userspace/seed.py, tinyagentos/userspace/seed/welcome/*, tests/userspace/conftest.py, tests/userspace/test_routes.py, tests/userspace/test_trust.py, tests/userspace/test_seed.py, tests/userspace/test_sdk_route.py, tests/userspace/test_e2e.py, tests/userspace/test_install_security.py, tests/userspace/test_broker_route.py, tests/userspace/test_update_consent.py, tests/userspace/test_immutability.py
Adds userspace install/list/permissions/enable/disable/uninstall/bundle/icon/broker routes, app lifecycle wiring and boot seeding, the browser SDK and seeded welcome app, and end-to-end route/security coverage.
Desktop userspace registry, launch surfaces, and iframe bridge
desktop/src/lib/userspace-apps.ts, desktop/src/lib/__tests__/userspace-apps.test.ts, desktop/src/registry/app-registry.ts, desktop/src/hooks/use-installed-userspace-apps.ts, desktop/src/components/Launchpad.tsx, desktop/src/apps/SandboxedAppWindow.tsx, desktop/src/apps/__tests__/SandboxedAppWindow.test.tsx
Adds desktop userspace manifest mapping and sync, fetches installed userspace apps into Launchpad, and bridges sandboxed iframe messages to the userspace broker with tests.

Project members and messaging fixes

Layer / File(s) Summary
External agent project membership
desktop/src/apps/ProjectsApp/AddAgentDialog.tsx, desktop/src/apps/ProjectsApp/ProjectMembers.tsx
Fetches active external agents, allows adding them from the dialog, and renders external project members in a separate section with restricted controls.
Messages channel refresh and panel positioning
desktop/src/apps/MessagesApp.tsx, desktop/src/apps/chat/ChannelSettingsPanel.tsx
Refreshes channels after creation, scopes sidebar grouping by current project, adds relative positioning to the wrapper, falls back to channel description for topic, and positions the settings panel absolutely.

Update suppression, docs, and repository maintenance

Layer / File(s) Summary
Docs-only update suppression
tinyagentos/auto_update.py, tinyagentos/routes/settings.py, tests/test_docs_only_update.py
Adds documentation-only change detection and suppresses update availability and notifications when remote changes are docs-only.
Repository docs and config updates
README.md, docs/STATUS.md, .github/dependabot.yml, desktop/node_modules
Adds a Discord badge to the README, refreshes status tracking, switches Dependabot Python updates to uv, and updates desktop/node_modules as a symlink.

Sequence Diagram(s)

sequenceDiagram
  participant Desktop as Desktop shell
  participant Iframe as Userspace iframe
  participant API as userspace_apps router
  participant Broker as userspace broker
  participant Store as userspace data store

  Desktop->>Iframe: load sandboxed bundle + theme tokens
  Iframe->>Desktop: postMessage capability request
  Desktop->>API: POST /api/userspace-apps/{appId}/broker
  API->>Broker: handle_capability(...)
  Broker->>Store: app-scoped kv/table/file operation
  Store-->>Broker: result
  Broker-->>API: broker response
  API-->>Desktop: JSON result
  Desktop-->>Iframe: postMessage reply
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

  • jaylfc/taOS#966: Touches the same creative studio app shells, Store studios surfaces, and related desktop UI modules.
  • jaylfc/taOS#970: Modifies the same SandboxedAppWindow broker bridge and related iframe messaging tests.
  • jaylfc/taOS#982: Changes the same docs-only update detection flow in tinyagentos/auto_update.py, tinyagentos/routes/settings.py, and corresponding tests.

Poem

🐇 I hopped through studios, shiny and new,
with tiny app windows and update cards too.
A sandbox now whispers through postMessage light,
while docs-only puffs drift away in the night.
Carrots for all—what a bustling PR sight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.36% 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 "Promote dev to master" accurately describes the main action of merging 40 commits from the dev branch to master, and is a common practice in release workflows.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 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 and usage tips.

Comment thread tinyagentos/userspace/url_guard.py Outdated
Comment on lines +19 to +33
def is_safe_public_url(url: str) -> bool:
"""True only if url is http(s) and every resolved IP is a public address.

Rejects private, loopback, link-local, reserved, unspecified and multicast
addresses (covers 127/8, ::1, 10/8, 172.16/12, 192.168/16, 169.254/16,
0.0.0.0, etc.).
"""
try:
parsed = urlparse(url)
except ValueError:
return False
if parsed.scheme not in _ALLOWED_SCHEMES or not parsed.hostname:
return False
try:
infos = socket.getaddrinfo(parsed.hostname, parsed.port or None)

@gitar-bot gitar-bot Bot Jun 17, 2026

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: SSRF guard bypassable via DNS rebinding (TOCTOU)

is_safe_public_url() resolves the hostname with socket.getaddrinfo and validates the resulting IPs, but the subsequent fetch in install_app (httpx.AsyncClient(...).get(url)) performs its OWN independent DNS resolution. An attacker who controls a domain can return a public IP during the guard check and a blocked internal address (e.g. 169.254.169.254 cloud-metadata, 127.0.0.1, RFC1918) at fetch time — the classic DNS-rebinding TOCTOU. follow_redirects=False does not close this gap because the very first request re-resolves the host.

The guard correctly handles redirects and IP-literal hosts, but the time-of-check vs time-of-use split means the validation result is not the address actually connected to.

Fix: resolve once, validate the resolved IP(s), then connect to that pinned IP rather than re-resolving. One approach is to pass the validated IP to httpx and set the original Host header / SNI, or use a custom httpx transport/resolver that returns only the pre-validated address. The guard should return the resolved IP so the caller can pin it.

Resolve+validate once and connect to the pinned IP to eliminate the rebinding window.:

# url_guard: return the validated IP so the caller can pin it
def resolve_safe_public_ip(url: str) -> str | None:
    parsed = urlparse(url)
    if parsed.scheme not in _ALLOWED_SCHEMES or not parsed.hostname:
        return None
    try:
        infos = socket.getaddrinfo(parsed.hostname, parsed.port or None)
    except (socket.gaierror, UnicodeError, OSError):
        return None
    chosen = None
    for info in infos:
        ip = ip_address(info[4][0])
        if (ip.is_private or ip.is_loopback or ip.is_link_local
                or ip.is_reserved or ip.is_unspecified or ip.is_multicast):
            return None
        chosen = chosen or str(ip)
    return chosen
# caller: connect to the pinned IP, keep Host header = original hostname

Was this helpful? React with 👍 / 👎

Comment on lines +36 to +50
if capability == "app.kv.get":
return {"result": await data_store.kv_get(app_id, args["key"])}
if capability == "app.kv.set":
await data_store.kv_set(app_id, args["key"], args.get("value"))
return {"result": True}
if capability == "app.kv.delete":
await data_store.kv_delete(app_id, args["key"])
return {"result": True}
if capability == "app.kv.keys":
return {"result": await data_store.kv_keys(app_id)}
if capability == "app.table.insert":
return {"result": await data_store.table_insert(app_id, args["table"], args.get("row", {}))}
if capability == "app.table.query":
return {"result": await data_store.table_query(app_id, args["table"], args.get("where"))}
if capability == "app.table.delete":

@gitar-bot gitar-bot Bot Jun 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: Broker capability args missing required keys cause 500s

Several capability handlers index required args directly: args["key"] (app.kv.get/set/delete), args["table"] (app.table.insert/query/delete) and args["id"] (app.table.delete). A sandboxed app that sends a malformed message (valid capability but omitting a required key) triggers an unhandled KeyError, which surfaces as an HTTP 500 from /broker rather than a structured {"error": ...} response. Combined with body = await request.json() in the broker route having no try/except (unlike install_app/set_permissions), invalid JSON from the iframe also produces a 500. These are reachable from untrusted community apps and are inconsistent with the graceful error contract the rest of the broker follows.

Suggest validating/.get()-ing required args and returning {"error": "invalid_args", ...}, and wrapping the broker route's request.json() in the same try/except used by the other endpoints.

Validate required args and return a structured error instead of raising KeyError.:

if capability == "app.kv.get":
    key = args.get("key")
    if not isinstance(key, str):
        return {"error": "invalid_args", "capability": capability}
    return {"result": await data_store.kv_get(app_id, key)}
# ...apply the same guard pattern to kv.set/delete and table.insert/query/delete

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

Note

Your trial team has used its Gitar budget, so automatic reviews are paused. Upgrade now to unlock full capacity. Comment "Gitar review" to trigger a review manually.
Learn more about usage limits

Code Review ⚠️ Changes requested 0 resolved / 2 findings

Promotes development branch features to master, but requires fixing a TOCTOU SSRF vulnerability in URL validation and adding missing key validation in broker capability handlers to prevent 500 errors.

⚠️ Security: SSRF guard bypassable via DNS rebinding (TOCTOU)

📄 tinyagentos/userspace/url_guard.py:19-33 📄 tinyagentos/routes/userspace_apps.py:91-101

is_safe_public_url() resolves the hostname with socket.getaddrinfo and validates the resulting IPs, but the subsequent fetch in install_app (httpx.AsyncClient(...).get(url)) performs its OWN independent DNS resolution. An attacker who controls a domain can return a public IP during the guard check and a blocked internal address (e.g. 169.254.169.254 cloud-metadata, 127.0.0.1, RFC1918) at fetch time — the classic DNS-rebinding TOCTOU. follow_redirects=False does not close this gap because the very first request re-resolves the host.

The guard correctly handles redirects and IP-literal hosts, but the time-of-check vs time-of-use split means the validation result is not the address actually connected to.

Fix: resolve once, validate the resolved IP(s), then connect to that pinned IP rather than re-resolving. One approach is to pass the validated IP to httpx and set the original Host header / SNI, or use a custom httpx transport/resolver that returns only the pre-validated address. The guard should return the resolved IP so the caller can pin it.

Resolve+validate once and connect to the pinned IP to eliminate the rebinding window.
# url_guard: return the validated IP so the caller can pin it
def resolve_safe_public_ip(url: str) -> str | None:
    parsed = urlparse(url)
    if parsed.scheme not in _ALLOWED_SCHEMES or not parsed.hostname:
        return None
    try:
        infos = socket.getaddrinfo(parsed.hostname, parsed.port or None)
    except (socket.gaierror, UnicodeError, OSError):
        return None
    chosen = None
    for info in infos:
        ip = ip_address(info[4][0])
        if (ip.is_private or ip.is_loopback or ip.is_link_local
                or ip.is_reserved or ip.is_unspecified or ip.is_multicast):
            return None
        chosen = chosen or str(ip)
    return chosen
# caller: connect to the pinned IP, keep Host header = original hostname
💡 Edge Case: Broker capability args missing required keys cause 500s

📄 tinyagentos/userspace/broker.py:36-50 📄 tinyagentos/routes/userspace_apps.py:241

Several capability handlers index required args directly: args["key"] (app.kv.get/set/delete), args["table"] (app.table.insert/query/delete) and args["id"] (app.table.delete). A sandboxed app that sends a malformed message (valid capability but omitting a required key) triggers an unhandled KeyError, which surfaces as an HTTP 500 from /broker rather than a structured {"error": ...} response. Combined with body = await request.json() in the broker route having no try/except (unlike install_app/set_permissions), invalid JSON from the iframe also produces a 500. These are reachable from untrusted community apps and are inconsistent with the graceful error contract the rest of the broker follows.

Suggest validating/.get()-ing required args and returning {"error": "invalid_args", ...}, and wrapping the broker route's request.json() in the same try/except used by the other endpoints.

Validate required args and return a structured error instead of raising KeyError.
if capability == "app.kv.get":
    key = args.get("key")
    if not isinstance(key, str):
        return {"error": "invalid_args", "capability": capability}
    return {"result": await data_store.kv_get(app_id, key)}
# ...apply the same guard pattern to kv.set/delete and table.insert/query/delete
🤖 Prompt for agents
Code Review: Promotes development branch features to master, but requires fixing a TOCTOU SSRF vulnerability in URL validation and adding missing key validation in broker capability handlers to prevent 500 errors.

1. ⚠️ Security: SSRF guard bypassable via DNS rebinding (TOCTOU)
   Files: tinyagentos/userspace/url_guard.py:19-33, tinyagentos/routes/userspace_apps.py:91-101

   `is_safe_public_url()` resolves the hostname with `socket.getaddrinfo` and validates the resulting IPs, but the subsequent fetch in `install_app` (`httpx.AsyncClient(...).get(url)`) performs its OWN independent DNS resolution. An attacker who controls a domain can return a public IP during the guard check and a blocked internal address (e.g. 169.254.169.254 cloud-metadata, 127.0.0.1, RFC1918) at fetch time — the classic DNS-rebinding TOCTOU. `follow_redirects=False` does not close this gap because the very first request re-resolves the host.
   
   The guard correctly handles redirects and IP-literal hosts, but the time-of-check vs time-of-use split means the validation result is not the address actually connected to.
   
   Fix: resolve once, validate the resolved IP(s), then connect to that pinned IP rather than re-resolving. One approach is to pass the validated IP to httpx and set the original Host header / SNI, or use a custom httpx transport/resolver that returns only the pre-validated address. The guard should return the resolved IP so the caller can pin it.

   Fix (Resolve+validate once and connect to the pinned IP to eliminate the rebinding window.):
   # url_guard: return the validated IP so the caller can pin it
   def resolve_safe_public_ip(url: str) -> str | None:
       parsed = urlparse(url)
       if parsed.scheme not in _ALLOWED_SCHEMES or not parsed.hostname:
           return None
       try:
           infos = socket.getaddrinfo(parsed.hostname, parsed.port or None)
       except (socket.gaierror, UnicodeError, OSError):
           return None
       chosen = None
       for info in infos:
           ip = ip_address(info[4][0])
           if (ip.is_private or ip.is_loopback or ip.is_link_local
                   or ip.is_reserved or ip.is_unspecified or ip.is_multicast):
               return None
           chosen = chosen or str(ip)
       return chosen
   # caller: connect to the pinned IP, keep Host header = original hostname

2. 💡 Edge Case: Broker capability args missing required keys cause 500s
   Files: tinyagentos/userspace/broker.py:36-50, tinyagentos/routes/userspace_apps.py:241

   Several capability handlers index required args directly: `args["key"]` (app.kv.get/set/delete), `args["table"]` (app.table.insert/query/delete) and `args["id"]` (app.table.delete). A sandboxed app that sends a malformed message (valid capability but omitting a required key) triggers an unhandled `KeyError`, which surfaces as an HTTP 500 from `/broker` rather than a structured `{"error": ...}` response. Combined with `body = await request.json()` in the broker route having no try/except (unlike `install_app`/`set_permissions`), invalid JSON from the iframe also produces a 500. These are reachable from untrusted community apps and are inconsistent with the graceful error contract the rest of the broker follows.
   
   Suggest validating/`.get()`-ing required args and returning `{"error": "invalid_args", ...}`, and wrapping the broker route's `request.json()` in the same try/except used by the other endpoints.

   Fix (Validate required args and return a structured error instead of raising KeyError.):
   if capability == "app.kv.get":
       key = args.get("key")
       if not isinstance(key, str):
           return {"error": "invalid_args", "capability": capability}
       return {"result": await data_store.kv_get(app_id, key)}
   # ...apply the same guard pattern to kv.set/delete and table.insert/query/delete

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

jaylfc added 3 commits June 17, 2026 21:40
List active registry agents under External / Connected agents and add
them as native members by handle when selected.
Wait for the external registry to load before classifying unknown
members into the main list, avoiding a brief flash when external
agents jump sections. Hide canvas and lead controls on external rows.
…tasks shipped), update method tested, promotion held on SSRF

@kilo-code-bot kilo-code-bot 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.

Initial read-only review comments.

Comment thread desktop/node_modules
@@ -0,0 +1 @@
/Volumes/NVMe/Users/jay/Development/tinyagentos/desktop/node_modules No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is an absolute symlink to /Volumes/NVMe/User. Committing host-specific symlinks will break checkouts on other machines and can leak local environment assumptions. Remove the symlink and add node_modules to .gitignore instead.

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


# Relaxed CSP for first-party packages (studios). Still sandboxed -- NEVER
# add allow-same-origin; that would collapse the opaque-origin isolation and
# let the frame access session cookies. The relaxations over community:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If the request body contains "granted": null, body.get("granted", []) returns None and this list comprehension raises TypeError, producing a 500. Validate that granted is a list of strings before iterating.

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

"connect-src 'self'; "
"frame-ancestors 'self'; base-uri 'none'"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This broker route calls request.json() without handling invalid JSON or non-object JSON. A malformed broker request can therefore raise and return a 500 instead of a controlled 400 response.

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

store = request.app.state.userspace_apps
if package is not None:
data = await package.read(_MAX_PACKAGE_BYTES + 1)
else:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Raw app_id is used to construct a filesystem path. Since app_id comes from the path parameter, validate it against the installed app record or use a store-owned app directory before resolving/deleting; otherwise crafted ids can target sibling app directories.

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

Comment thread tinyagentos/routes/userspace_apps.py Outdated
url = body.get("source_url")
if not url:
return JSONResponse({"error": "source_url or package required"}, status_code=400)
# SSRF guard: only fetch public http(s) hosts, and do not follow

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same concern here: raw app_id is resolved into a bundle-serving path before validating that it identifies the installed app. A crafted id with path separators can target another app directory under the same root.

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

(app_id, table, json.dumps(row)))
await self._db.commit()
return cur.lastrowid

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

where is assumed to be a mapping. If an app calls table.query(table, where="x") or passes a list, this raises AttributeError and returns a 500. Validate where is None or a mapping before iterating.

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

def parse_manifest(text: str) -> dict:
try:
data = yaml.safe_load(text) or {}
except yaml.YAMLError as exc:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

decode("utf-8") can raise UnicodeDecodeError, which is not caught here. Catch it and return PackageError("manifest.yaml is not valid UTF-8") so malformed packages get a controlled 400 response.

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

try:
zf = zipfile.ZipFile(io.BytesIO(data))
except zipfile.BadZipFile as exc:
raise PackageError("not a valid .taosapp (zip) archive") from exc

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Required manifest fields are only checked for truthiness. A manifest like id: 123 passes this check but can cause TypeError later when building apps_root / manifest["id"]. Validate required fields as strings, and validate permissions as a list of strings.

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

# app_dir itself must stay within apps_root (defends against a crafted id)
if not app_dir.is_relative_to(apps_root) or app_dir == apps_root:
raise PackageError(f"unsafe path in package: id {manifest['id']!r}")
app_dir.mkdir(parents=True, exist_ok=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This extracts members directly into app_dir while iterating. If a later member is rejected, the app directory may be left partially populated. Pre-validate all member paths first, or extract to a temporary directory and atomically move it into place only after the package is fully accepted.

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

@kilo-code-bot

kilo-code-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 13 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 2
WARNING 10
SUGGESTION 1

Incremental review covered the latest docs-only update suppression changes. No previous issues were resolved in the new commit; the 11 earlier findings are carried forward below.

Fix link: Fix these issues in Kilo Cloud

Issue Details (click to expand)

CRITICAL

File Line Issue
tinyagentos/auto_update.py 228 git diff --name-only can miss mode-only changes, so a commit that changes docs plus only executable bits on code files can be misclassified as docs-only.
tinyagentos/userspace/url_guard.py 33 SSRF guard resolves DNS separately from the subsequent fetch, allowing DNS rebinding to bypass public-host validation.
tinyagentos/routes/userspace_apps.py 81 Raw app_id is used to construct a deletion path before validating the installed app identity, allowing crafted ids to target sibling app directories.

WARNING

File Line Issue
tinyagentos/userspace/broker.py 50 Required broker capability args are indexed directly; malformed capability payloads can raise KeyError and return 500.
desktop/node_modules 1 Host-specific absolute symlink should not be committed.
tinyagentos/routes/userspace_apps.py 38 granted: null causes a TypeError in the permissions list comprehension.
tinyagentos/routes/userspace_apps.py 54 Broker route calls request.json() without handling invalid JSON or non-object bodies.
tinyagentos/routes/userspace_apps.py 89 Raw app_id is resolved into a bundle-serving path before validating the installed app identity.
tinyagentos/userspace/data_store.py 61 where is assumed to be a mapping; non-mapping values can raise AttributeError.
tinyagentos/userspace/package.py 25 Manifest UTF-8 decode can raise uncaught UnicodeDecodeError.
tinyagentos/userspace/package.py 63 Required manifest fields are checked only for truthiness and are not type-validated.
tinyagentos/userspace/package.py 86 Package extraction writes directly into the final app directory and can leave partial state if a later member is rejected.

SUGGESTION

File Line Issue
tinyagentos/auto_update.py 336 Docs-only skip returns before poll_frameworks(...), delaying framework release checks until the next cycle.
Other Observations (not in diff)

Unchanged files carried forward from the previous review:

File Line Issue
tinyagentos/userspace/url_guard.py 33 SSRF guard resolves DNS separately from the subsequent fetch, allowing DNS rebinding to bypass public-host validation.
tinyagentos/userspace/broker.py 50 Required broker capability args are indexed directly; malformed capability payloads can raise KeyError and return 500.
desktop/node_modules 1 Host-specific absolute symlink should not be committed.
tinyagentos/routes/userspace_apps.py 38 granted: null causes a TypeError in the permissions list comprehension.
tinyagentos/routes/userspace_apps.py 54 Broker route calls request.json() without handling invalid JSON or non-object bodies.
tinyagentos/routes/userspace_apps.py 81 Raw app_id is used to construct a deletion path before validating the installed app identity.
tinyagentos/routes/userspace_apps.py 89 Raw app_id is resolved into a bundle-serving path before validating the installed app identity.
tinyagentos/userspace/data_store.py 61 where is assumed to be a mapping; non-mapping values can raise AttributeError.
tinyagentos/userspace/package.py 25 Manifest UTF-8 decode can raise uncaught UnicodeDecodeError.
tinyagentos/userspace/package.py 63 Required manifest fields are checked only for truthiness and are not type-validated.
tinyagentos/userspace/package.py 86 Package extraction writes directly into the final app directory and can leave partial state if a later member is rejected.
Files Reviewed (3 files)
  • tinyagentos/auto_update.py - 2 issues
  • tinyagentos/routes/settings.py - 0 issues
  • tests/test_docs_only_update.py - 0 issues
Previous Review Summary (commit 9bfd008)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 9bfd008)

Summary

Reviewed PR #979 in read-only mode after fetching the latest PR state, diff, and existing comments. I posted 9 new inline review comments focused on security, runtime error handling, and package extraction safety.

New Review Comments

  • desktop/node_modules:1: host-specific absolute symlink should not be committed.
  • tinyagentos/routes/userspace_apps.py:38: granted: null can cause a 500.
  • tinyagentos/routes/userspace_apps.py:54: broker route can 500 on malformed JSON.
  • tinyagentos/routes/userspace_apps.py:81 and :89: raw app_id is used in filesystem paths without validating the installed app identity.
  • tinyagentos/userspace/data_store.py:61: table where is assumed to be a mapping.
  • tinyagentos/userspace/package.py:25: manifest UTF-8 decode can raise uncaught UnicodeDecodeError.
  • tinyagentos/userspace/package.py:63: manifest required fields are not type-validated.
  • tinyagentos/userspace/package.py:86: package extraction can leave partial state if a later member is rejected.

Existing Review Comments

Existing comments on tinyagentos/userspace/url_guard.py:33 and tinyagentos/userspace/broker.py:50 were preserved and not duplicated.

Review Status

This PR needs fixes before merge. The highest-risk items are raw app_id path construction, package extraction/persistence before full validation, and malformed request handling that can return 500s.

Kilo fix link: https://app.kilo.ai/cloud-agent-fork/review/a9f07fc0-3d6a-4142-8a1c-0b5543897419


Reviewed by nex-n2-pro:free · 1,340,108 tokens

When the tracked branch tip only changes documentation (docs/ or *.md),
suppress the Update available notification and has_updates=false on
update-check. Code or mixed diffs still report normally.
paths = [line.strip() for line in out.splitlines() if line.strip()]
if not paths:
return False
return all(is_documentation_path(p) for p in paths)

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: git diff --name-only can miss mode-only changes

changes_are_docs_only() classifies a diff from file names alone. If a commit changes documentation plus only the executable bit of a code file, --name-only will not list the code file, so the mixed commit can be treated as docs-only. Use git diff --name-status or --summary and treat non-documentation paths or mode changes as non-doc to keep update suppression conservative.

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

(current or "")[:7],
new_commit[:7],
)
return

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: Docs-only skip short-circuits framework polling

When changes_are_docs_only() returns true, _run_once() returns before poll_frameworks(...). That delays framework release checks until the next cycle whenever the tracked branch has a docs-only commit. Move framework polling before the docs-only return, or return only from the repository-update notification block, so framework polling remains independent of repo commit classification.

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

jaylfc added 3 commits June 17, 2026 22:54
Treat paths as documentation only when they end in a doc extension or
live under docs/ without a code/config extension, so files like
docs/scripts/foo.py no longer suppress real update notifications.
…#971)

The install-from-URL path validated source_url against private/loopback/
link-local ranges, then let httpx re-resolve the hostname at fetch time. A
host that resolved public during the check could resolve to an internal IP
at connect time (DNS-rebind TOCTOU), defeating the guard.

resolve_safe_public_ip() now resolves the host once and returns a validated
public IP. The install fetch connects to that pinned IP and carries the
original Host header + TLS SNI, so vhost routing and cert validation still
work but the client never re-resolves. follow_redirects stays off.

Also centralises required-arg validation in the broker so a missing key
returns {"error": "missing_arg"} instead of an uncaught KeyError (500).
fix(userspace): pin install fetch to validated IP (DNS-rebind SSRF #971)

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

Note

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

Caution

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

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

123-137: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The test does not verify the “no extracted directory remains” security guarantee.

This test only checks DB state. It should also verify the rejected container bundle is not retrievable, otherwise filesystem leftovers can pass unnoticed.

Proposed test hardening
 async def test_container_install_rejected_with_no_stored_state(client):
@@
     # No app row stored.
     rows = (await client.get("/api/userspace-apps")).json()
     assert all(a["app_id"] != "ctapp" for a in rows)
+    # No bundle content should be reachable after rejection.
+    bundle = await client.get("/api/userspace-apps/ctapp/bundle/index.html")
+    assert bundle.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 `@tests/userspace/test_routes.py` around lines 123 - 137, The test function
test_container_install_rejected_with_no_stored_state verifies that no app row is
stored in the database after a rejected container installation, but it does not
verify the corresponding filesystem guarantee that no extracted directory
remains. Add an assertion or verification step after the database check to
confirm that the extracted container bundle directory is not accessible or does
not exist, ensuring the complete security guarantee mentioned in the comment
about leaving no app row and no extracted directory behind.
♻️ Duplicate comments (1)
tinyagentos/routes/userspace_apps.py (1)

256-256: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unhandled JSON parsing error in broker endpoint.

await request.json() can raise on invalid or non-JSON request bodies, resulting in a 500 instead of a controlled 400 response. Wrap in try/except as done in other endpoints.

🐛 Proposed fix
 `@router.post`("/api/userspace-apps/{app_id}/broker")
 async def broker(request: Request, app_id: str):
     store = request.app.state.userspace_apps
     app = await store.get(app_id)
     if app is None or not app["enabled"]:
         return JSONResponse({"error": "app not found or disabled"}, status_code=404)
-    body = await request.json()
+    try:
+        body = await request.json()
+    except Exception:
+        return JSONResponse({"error": "invalid JSON body"}, status_code=400)
     # First-party apps have all gated capabilities pre-authorised -- no per-cap
🤖 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/userspace_apps.py` at line 256, The `await request.json()`
call in the broker endpoint lacks error handling for malformed or invalid JSON
in the request body. Wrap the line `body = await request.json()` in a try/except
block to catch JSON parsing exceptions and return a controlled 400 Bad Request
response instead of allowing the exception to propagate as a 500 error. Follow
the same pattern already implemented in other endpoints in the codebase for
consistency.
🟡 Minor comments (11)
desktop/src/apps/MessagesApp.tsx-2761-2761 (1)

2761-2761: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle empty-string topics in fallback logic.

Line 2761 uses ??, so topic: "" won’t fall back to description. That misses the “empty topic uses description” behavior and can still render blank topic text.

Proposed fix
-            topic: currentChannel.topic ?? currentChannel.description ?? "",
+            topic: (currentChannel.topic && currentChannel.topic.trim().length > 0)
+              ? currentChannel.topic
+              : (currentChannel.description ?? ""),
🤖 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/MessagesApp.tsx` at line 2761, The fallback logic for the
topic field at line 2761 uses the nullish coalescing operator `??` which only
handles null and undefined values, not empty strings. When currentChannel.topic
is an empty string, it will not fall back to currentChannel.description as
intended, resulting in blank topic text being rendered. Replace the nullish
coalescing operator with a truthy check (using the logical OR operator `||` or a
ternary conditional) that treats both null/undefined AND empty strings as falsy
values, ensuring that empty topics properly fall back to the description value.
desktop/src/apps/StoreApp/index.tsx-933-935 (1)

933-935: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Refresh optional catalog state after initial mount to avoid stale Updates results.

Line 933 fetches optionalCatalog only once, but it drives both the optional updates list and the “up to date” empty state later. In-session optional install/uninstall changes can leave this view stale until reload.

Also applies to: 1154-1156, 1182-1187

🤖 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/StoreApp/index.tsx` around lines 933 - 935, The optional
catalog is fetched only once on component mount, causing the state to become
stale when users install or uninstall optional apps during the session. Extract
the fetch logic that retrieves the optional catalog from the current useEffect
into a separate refresh function, then call this function not only during the
initial mount but also after optional app installations and uninstalls are
completed. This ensures the optionalCatalog state is refreshed in-session
whenever the app list changes, keeping the optional updates list and "up to
date" empty state synchronized with actual installations/uninstalls.
desktop/src/apps/StoreApp/index.tsx-1154-1176 (1)

1154-1176: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Apply the active search query to optional-update rows in the Updates tab.

Optional rows are rendered without search filtering, while the standard updates list is filtered. This can produce contradictory UI (optional rows visible while “No matches” is shown).

Suggested fix
-              {activeNav === "updates" && (() => {
-                const updatableOptional = optionalCatalog.filter((e) => e.installed && e.update_available);
+              {activeNav === "updates" && (() => {
+                const q = search.trim().toLowerCase();
+                const updatableOptional = optionalCatalog.filter((e) => {
+                  if (!e.installed || !e.update_available) return false;
+                  if (!searching) return true;
+                  const label = (getApp(e.id)?.name ?? e.id).toLowerCase();
+                  return label.includes(q);
+                });
                 if (updatableOptional.length === 0) return null;
                 return (
@@
-              ) : activeNav === "updates" && filtered.length === 0 ? (
-                optionalCatalog.some((e) => e.installed && e.update_available) ? null : (
+              ) : activeNav === "updates" && filtered.length === 0 ? (
+                optionalCatalog.some((e) => {
+                  if (!e.installed || !e.update_available) return false;
+                  if (!searching) return true;
+                  const label = (getApp(e.id)?.name ?? e.id).toLowerCase();
+                  return label.includes(search.trim().toLowerCase());
+                }) ? null : (

Also applies to: 1182-1187

🤖 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/StoreApp/index.tsx` around lines 1154 - 1176, The
updatableOptional list in the updates tab is not filtered by the active search
query, causing optional update rows to display even when the search produces no
matches elsewhere. Apply the same search filtering logic used for the main
filtered updates list to the updatableOptional filter. The
updatableOptional.filter call on line 1155 should include an additional
condition that matches against the search query in the same way the main updates
list is filtered. Also apply the same fix to the optional update section
mentioned at lines 1182-1187 to ensure consistency across all optional update
displays in the Updates tab.
desktop/src/apps/DesignStudioApp.tsx-66-69 (1)

66-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Elements rail item maps to DesignView instead of an elements surface.

Selecting Elements currently shows the Design surface, so navigation state and content are inconsistent. Add an ElementsView placeholder (or rename/remove the rail item) until the dedicated surface is ready.

Suggested minimal fix
-          {view === "elements" && <DesignView />}
+          {view === "elements" && (
+            <div className="flex flex-1 items-center justify-center text-sm text-shell-text-tertiary">
+              Elements view coming soon
+            </div>
+          )}
🤖 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/DesignStudioApp.tsx` around lines 66 - 69, The conditional
rendering in DesignStudioApp.tsx incorrectly maps the "elements" view to
DesignView instead of a dedicated ElementsView component. Replace the line that
renders DesignView when view equals "elements" with a corresponding ElementsView
component. Either create a new ElementsView placeholder component or use an
existing appropriate component for the elements surface. This ensures that
selecting the Elements rail item displays the correct content instead of
duplicating the Design view.
desktop/src/apps/CodingStudioApp.tsx-66-69 (1)

66-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Code rail item maps to the wrong surface.

Clicking Code renders BuildView, so two rail items lead to the same content. Either add a dedicated CodeView placeholder or temporarily remove/rename the rail item to avoid misleading navigation.

Suggested minimal fix
-          {view === "code" && <BuildView />}
+          {view === "code" && (
+            <div className="flex flex-1 items-center justify-center text-sm text-shell-text-tertiary">
+              Code view coming soon
+            </div>
+          )}
🤖 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/CodingStudioApp.tsx` around lines 66 - 69, The conditional
rendering in the CodingStudioApp component has a bug where the "code" view
renders the same BuildView component as the "build" view, creating duplicate
navigation paths. Replace the line that renders BuildView when view === "code"
with either a dedicated CodeView component or remove the code branch entirely if
CodeView is not yet implemented. Ensure each view option in the conditional
rendering maps to a unique content surface.
desktop/src/apps/officesuite/WriteView.tsx-113-115 (1)

113-115: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale studio names in the sample copy.

Lines 113-115 mention “Images Studio and Game Studio,” which does not match the studio set introduced here. This can confuse users during demos/tests.

Suggested fix
-              Images Studio and Game Studio are available now. Coding Studio is rolling out, with
-              Design, Music, App, and Office studios close behind. Each one installs from the Store
+              Coding, Design, Music, App, and Office studios are available now. Each one installs from the Store
               in a single click.
🤖 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/officesuite/WriteView.tsx` around lines 113 - 115, The
sample copy text in the WriteView component on lines 113-115 references outdated
studio names "Images Studio and Game Studio" that do not match the current
studio set being described in the surrounding text which mentions "Coding
Studio," "Design," "Music," "App," and "Office" studios. Update the text in this
section to reference the correct, current studio names instead of the stale ones
to ensure consistency and avoid confusing users during demos or testing.
README.md-224-224 (1)

224-224: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update Creative Studios wording to match shipped functionality.

Line 224 still says Coding/App/Design/Music/Office studios are “on the way,” but this PR introduces those studio apps as optional installable shells. Please revise this sentence so release docs reflect actual availability.

🤖 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 `@README.md` at line 224, The sentence in the README describes Coding, App,
Design, Music, and Office studios as "on the way," but this PR introduces them
as optional installable shells that are now available. Update the wording to
reflect that these studio apps are now available as optional installations
rather than future functionality, while keeping the clarification about App
Studio being taOS's own app builder.
desktop/src/apps/MusicStudioApp.tsx-56-68 (1)

56-68: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add active-state semantics for the Export rail button.

Line 56-68 should expose active navigation state like the other rail items; currently Export never sets aria-current, so assistive tech loses parity with Studio/Compose/Sounds/Mixer.

Suggested patch
           <button
             type="button"
             aria-label="Export"
+            aria-current={view === "export" ? "page" : undefined}
             onClick={() => setView("export")}
             className={`flex h-[46px] w-[46px] flex-col items-center justify-center gap-0.5 rounded-xl text-[9px] font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
🤖 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/MusicStudioApp.tsx` around lines 56 - 68, The Export button
in the MusicStudioApp component lacks active-state semantics that other rail
items provide. Add an aria-current attribute to the Export button that sets it
to "page" when view equals "export" and is false or omitted otherwise. This will
ensure assistive technologies can properly announce the active navigation state,
providing parity with the other rail buttons like Studio, Compose, Sounds, and
Mixer.
tests/userspace/test_data_store.py-7-7 (1)

7-7: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Split multi-statement lines to satisfy Ruff E702 and keep tests readable.

These two lines currently chain statements with semicolons.

Suggested fix
-    s = UserspaceDataStore(tmp_path / "d.db"); await s.init()
+    s = UserspaceDataStore(tmp_path / "d.db")
+    await s.init()
@@
-    s = UserspaceDataStore(tmp_path / "d.db"); await s.init()
+    s = UserspaceDataStore(tmp_path / "d.db")
+    await s.init()

Also applies to: 22-22

🤖 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/userspace/test_data_store.py` at line 7, Split the chained statements
separated by semicolons into separate lines. On line 7, split the
UserspaceDataStore instantiation and the await s.init() call into two distinct
lines. Apply the same refactoring to line 22 where a similar multi-statement
line exists with a semicolon separator. This will satisfy the Ruff E702 rule and
improve test readability.

Source: Linters/SAST tools

tests/userspace/test_broker.py-6-7 (1)

6-7: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Split the one-line helper statements to satisfy Ruff E702.

Proposed fix
 async def _store(tmp_path):
-    s = UserspaceDataStore(tmp_path / "d.db"); await s.init(); return s
+    s = UserspaceDataStore(tmp_path / "d.db")
+    await s.init()
+    return s
🤖 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/userspace/test_broker.py` around lines 6 - 7, The _store function
violates Ruff E702 by having multiple statements on a single line separated by
semicolons. Split the single line into three separate lines within the function
body, placing the UserspaceDataStore initialization, the await s.init() call,
and the return statement each on their own line.

Source: Linters/SAST tools

tinyagentos/routes/userspace_apps.py-26-54 (1)

26-54: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Community and first-party CSP are identical.

_BUNDLE_CSP and _BUNDLE_CSP_FIRST_PARTY are character-for-character identical. The comment mentions "relaxations over community" but none are actually applied. Either differentiate them as intended or consolidate into a single constant.

🤖 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/userspace_apps.py` around lines 26 - 54, The constants
_BUNDLE_CSP and _BUNDLE_CSP_FIRST_PARTY are currently identical despite the
detailed comment explaining intended relaxations for first-party packages.
Either consolidate these two constants into a single _BUNDLE_CSP constant if
they should be the same, or modify _BUNDLE_CSP_FIRST_PARTY to apply the
documented relaxations mentioned in the comment above it (such as the
connect-src and style-src adjustments) to differentiate it from the community
CSP policy.
🧹 Nitpick comments (7)
desktop/src/apps/ProjectsApp/AddAgentDialog.tsx (1)

60-72: Backend already protects against duplicate member additions via database constraints; consider optimizing the client-side guard to prevent redundant requests.

The render-snapshot-based submitting check allows rapid double-triggers to issue multiple addNative/addClone POSTs. However, the backend's ON CONFLICT(project_id, member_id) DO NOTHING clause silently deduplicates these requests at the database layer, preserving the original added_at timestamp. While data integrity is protected, preventing redundant calls improves efficiency and UX. The suggested fix using a synchronous useRef guard is a reasonable optimization to avoid wasted network requests.

🤖 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/ProjectsApp/AddAgentDialog.tsx` around lines 60 - 72, The
addExternal function's check for the submitting state is vulnerable to race
conditions on rapid clicks since state updates are asynchronous, allowing
multiple API calls to be issued before the state is actually updated. Add a
synchronous useRef guard that is checked and set immediately at the start of the
addExternal function before the submitting state check. Create a ref variable
that tracks whether a request is currently in flight and synchronously set it to
true before making the API call, then set it back to false in the finally block.
This synchronous guard will reliably prevent redundant addNative calls
regardless of how quickly the user clicks.
desktop/src/apps/MusicStudioApp.test.tsx (1)

63-79: ⚡ Quick win

Add explicit tests for Mixer and Export rail states.

Current suite exercises Studio/Compose/Sounds, but Line 63-79 still leaves mixer and export paths unverified. Add two small tests that click each rail item and assert both aria-current="page" and the expected placeholder text.

🤖 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/MusicStudioApp.test.tsx` around lines 63 - 79, Add two new
test cases following the same pattern as the existing "switches to Sounds view
on rail click" test to verify the Mixer and Export views. For each new test,
call renderApp(), use fireEvent.click() to click the respective rail button
(Mixer and Export), then verify that the aria-current attribute is set to "page"
on the navigation button and assert that the expected heading or content for
that view is defined using screen.getByRole() or screen.getByText(). This
ensures complete coverage of all rail navigation states.
tests/userspace/test_seed.py (2)

171-171: ⚡ Quick win

Avoid hardcoding the real bundled app version in this test.

Pinning "1.0.0" here creates avoidable churn whenever tinyagentos/userspace/seed/welcome/manifest.yaml is intentionally version-bumped.

🤖 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/userspace/test_seed.py` at line 171, The test assertion in the
test_seed.py file is hardcoding the version string "1.0.0" which creates
maintenance burden when the actual version in
tinyagentos/userspace/seed/welcome/manifest.yaml is updated. Instead of
hardcoding the version value in the assertion, dynamically load the version from
the manifest.yaml file and use that loaded value in the comparison. This way,
the test will automatically use the current version from the manifest without
requiring manual updates whenever the version is bumped.

79-89: ⚡ Quick win

Strengthen idempotency assertions against timestamp-granularity false passes.

installed_at uses second-level time resolution, so an unintended reseed can still pass if both writes happen within the same second. Consider patching time in this test to make regressions deterministic.

Also applies to: 183-190

🤖 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/userspace/test_seed.py` around lines 79 - 89, The installed_at
timestamp assertion is vulnerable to false passes due to second-level time
resolution granularity. Mock or patch the system time in the test to return
controlled, deterministic timestamps so that the first and second seed
operations would produce different installed_at values if an unintended reseed
actually occurred. Apply this time mocking to both the initial seeding operation
before the installed_at_first assertion and the subsequent seed_bundled_apps
call, ensuring they use different mocked timestamps to make the idempotency
check reliable.
tests/userspace/test_immutability.py (1)

7-9: 💤 Low value

Duplicate test coverage.

test_native_app_type_rejected duplicates the same test in tests/userspace/test_package.py:33-37. Consider removing this duplicate to avoid maintenance burden.

🤖 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/userspace/test_immutability.py` around lines 7 - 9, The
test_native_app_type_rejected function in this file is a duplicate of an
existing test in tests/userspace/test_package.py that covers the same
functionality. Remove the entire test_native_app_type_rejected function from the
current file to eliminate the duplicate test coverage and reduce maintenance
burden.
tests/userspace/conftest.py (1)

55-58: ⚡ Quick win

Fail fast if fixture auth bootstrap fails to create the admin user.

The empty-string fallback for user_id can silently hide broken fixture setup and produce invalid session state. Assert user creation succeeded before session creation.

Proposed change
     app.state.auth.setup_user("admin", "Test Admin", "", "testpass")
     record = app.state.auth.find_user("admin")
-    uid = record["id"] if record else ""
-    token = app.state.auth.create_session(user_id=uid, long_lived=True)
+    assert record is not None, "fixture setup failed: admin user missing"
+    token = app.state.auth.create_session(user_id=record["id"], long_lived=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 `@tests/userspace/conftest.py` around lines 55 - 58, The code silently defaults
uid to an empty string when find_user("admin") fails to find the user, which can
hide broken fixture setup and produce invalid session state. After the find_user
call that retrieves the record, add an assertion to ensure the record exists and
is not None before extracting the id value. This will cause the fixture to fail
fast and clearly indicate that admin user creation or lookup failed, rather than
proceeding with an invalid empty-string user_id to the create_session call.
tests/userspace/test_update_consent.py (1)

21-25: ⚡ Quick win

Make the permission-delta assertion exact.

"app.memory" in new_permissions can still pass if previously granted permissions are incorrectly re-flagged. Assert status and exact delta.

Proposed assertion tightening
     r = await client.post("/api/userspace-apps/install",
                           files={"package": ("t.taosapp", _zip("[app.net, app.memory]"), "application/zip")})
+    assert r.status_code == 200, r.text
     body = r.json()
     assert body["needs_consent"] is True
-    assert "app.memory" in body["new_permissions"]
+    assert set(body["new_permissions"]) == {"app.memory"}
🤖 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/userspace/test_update_consent.py` around lines 21 - 25, The assertion
for body["new_permissions"] is too loose and only checks that "app.memory"
exists within the set, which doesn't catch if previously granted permissions are
incorrectly re-flagged as new. Replace the membership check with an exact
assertion that verifies new_permissions equals exactly the expected set of
permissions, ensuring only the truly new permission "app.memory" is included and
no previously granted permissions are incorrectly present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@desktop/node_modules`:
- Line 1: The desktop/node_modules directory is currently a tracked symbolic
link pointing to an absolute macOS-specific path that will break the repository
on other machines and CI/CD environments. Remove this symlink from git tracking
by using git rm --cached on the desktop/node_modules entry, then add
desktop/node_modules/ to the .gitignore file to prevent it from being tracked in
the future. After making these changes, verify that the project builds
successfully by running npm ci in the desktop directory to ensure dependencies
are properly installed without relying on the symlink.

In `@desktop/src/apps/designstudio/MagicView.tsx`:
- Around line 79-93: The interactive result tiles are currently rendered as div
elements instead of semantic button elements, making them inaccessible for
keyboard navigation. Replace the div element that wraps each result in the
RESULTS.map function (the one with key={label} and the className containing
"cursor-pointer") with a button element. Preserve all existing className and
style attributes, and ensure the button maintains the same visual appearance and
interactivity as the current div implementation.

In `@desktop/src/apps/officesuite/SlidesView.tsx`:
- Around line 30-52: Replace the div element that has role="button" with a
native HTML button element. The button should retain the key prop, tabIndex,
aria-label, className, and style attributes from the original div. Remove the
role="button" attribute since it's now redundant on a real button element. Keep
the span child element that displays the slide index and the s.label text
content as-is. This will automatically provide full keyboard support
(Enter/Space handling) and better accessibility without requiring manual event
handling.

In `@desktop/src/apps/ProjectsApp/ProjectMembers.tsx`:
- Around line 107-143: Add error handling and busy state management to the three
async handlers in the member permission UI. For the "Can edit canvas" checkbox
onChange handler (calling canvasApi.setPermission), the "Lead" checkbox onChange
handler (calling projectsApi.members.setLead), and the remove button onClick
handler (calling projectsApi.members.remove), wrap each API call in try-catch
blocks to handle errors gracefully, add a loading/busy state variable to disable
the input elements during the API call, and display error feedback to the user
when the API call fails so the UI remains consistent and prevents duplicate
requests from rapid clicks.

In `@desktop/src/apps/SettingsApp/UpdatesPanel.tsx`:
- Line 3: The namespace import of `lucide-react` in UpdatesPanel.tsx forces the
entire icon library into the bundle even though only specific optional-app icons
are used. Replace the `import * as LucideIcons` statement with explicit named
imports of only the icons actually referenced in the optional-app icon lookup
logic (around lines 41-49), then create a simple object map that maps icon names
to the imported icon components, eliminating the need for dynamic pascal-case
lookup via LucideIcons[].

In `@desktop/src/lib/__tests__/userspace-apps.test.ts`:
- Around line 5-8: The test assertions for the toAppManifest function are
expecting raw app IDs like "todo", but the function now returns namespaced IDs
in the format "userspace:${app_id}". Update the expect statements that check
m.id to expect "userspace:todo" instead of "todo" at both locations mentioned
(around line 7 and line 42) to match the current behavior of toAppManifest.

In `@desktop/src/registry/app-registry.ts`:
- Around line 203-222: The syncUserspaceApps function only appends new userspace
manifests but never updates existing ones, which prevents refreshed metadata
(name, icon), component closures, and trust information from being applied after
catalog changes. Modify the logic to not only add missing manifests with
apps.push(m), but also replace existing userspace manifests in the apps array
with their updated counterparts from the incoming manifests map to ensure
metadata and component closures are kept current.

In `@tinyagentos/auto_update.py`:
- Around line 203-221: The _DOC_EXTENSIONS tuple includes .txt which is too
broad and can incorrectly classify non-documentation files like dependency lists
or runtime configuration files as documentation, suppressing legitimate updates.
Remove the .txt extension from the _DOC_EXTENSIONS tuple to make the
is_documentation_path function classifier more conservative and only treat true
documentation file types as documentation-only paths.

In `@tinyagentos/userspace/broker.py`:
- Around line 34-46: The argument validation in the capability dispatcher only
checks for key presence in the _required dictionary but does not validate
argument types, allowing malformed values (non-string keys, non-int ids,
non-dict headers) to cause uncaught exceptions downstream. Extend the validation
logic that iterates through _required.get(capability, ()) to also check that
each argument has the correct expected type (e.g., key/path as string, id as
integer, headers as dict) and return a clean error response with
"invalid_arg_type" error when type validation fails, similar to the existing
missing_arg error handling.

In `@tinyagentos/userspace/data_store.py`:
- Line 72: The dictionary construction in the append statement for the out list
uses the wrong order of precedence, allowing the "id" key from the data
dictionary to override the authoritative rid value. In the line where out.append
is called with a dictionary containing "id": rid and **data unpacked, reverse
the order of precedence by placing the authoritative rid assignment after the
data unpacking so that {**data, "id": rid} ensures rid always takes precedence
over any "id" key that might exist in the data dictionary.

In `@tinyagentos/userspace/sdk/taos-app-sdk.js`:
- Around line 12-19: The message event listener in the anonymous callback does
not validate the origin of incoming messages before resolving pending RPC calls.
Add an origin validation check on the event object (e.origin) before processing
the message and resolving the promise. Only proceed with resolving the pending
call when the message originates from a trusted/expected origin, preventing
spoofed messages from different frames from hijacking in-flight RPC calls.
- Around line 29-34: The call function creates pending promises that are stored
in the pending map but have no timeout mechanism, causing memory leaks if broker
responses never arrive. Add a timeout handler within the call function that
rejects the promise and removes the entry from the pending map after a
reasonable duration (such as 30 seconds). Use setTimeout to trigger this
cleanup, and ensure the timeout is cleared if the response arrives before the
timeout expires by storing the timeout ID alongside the resolve function in the
pending map entry.
- Around line 40-67: The gated capability methods like agent.ask and
memory.search currently extract and return only r.result, which causes
permission_denied error responses to become undefined or hidden from callers.
Normalize error handling by modifying the gated capability methods (agent.ask at
line 62 and memory.search at line 63) to return the full response envelope
instead of extracting r.result, ensuring that error objects like {error:
"permission_denied"} are properly visible to callers. This should be consistent
with how other methods handle responses to provide uniform error detection
across all SDK methods.

In `@tinyagentos/userspace/seed.py`:
- Around line 68-73: The shutil.rmtree call in the re-seed block uses app_id to
construct the path without first validating that it stays within apps_root,
creating a security vulnerability where a malicious manifest id could delete
files outside the intended directory. Add path validation before the
shutil.rmtree call (before line 71) to ensure the resolved path stays within
apps_root, similar to how extract_package validates paths. Check that resolving
apps_root / app_id results in a path that is within apps_root and reject or
sanitize app_id values that would escape the directory.

---

Outside diff comments:
In `@tests/userspace/test_routes.py`:
- Around line 123-137: The test function
test_container_install_rejected_with_no_stored_state verifies that no app row is
stored in the database after a rejected container installation, but it does not
verify the corresponding filesystem guarantee that no extracted directory
remains. Add an assertion or verification step after the database check to
confirm that the extracted container bundle directory is not accessible or does
not exist, ensuring the complete security guarantee mentioned in the comment
about leaving no app row and no extracted directory behind.

---

Minor comments:
In `@desktop/src/apps/CodingStudioApp.tsx`:
- Around line 66-69: The conditional rendering in the CodingStudioApp component
has a bug where the "code" view renders the same BuildView component as the
"build" view, creating duplicate navigation paths. Replace the line that renders
BuildView when view === "code" with either a dedicated CodeView component or
remove the code branch entirely if CodeView is not yet implemented. Ensure each
view option in the conditional rendering maps to a unique content surface.

In `@desktop/src/apps/DesignStudioApp.tsx`:
- Around line 66-69: The conditional rendering in DesignStudioApp.tsx
incorrectly maps the "elements" view to DesignView instead of a dedicated
ElementsView component. Replace the line that renders DesignView when view
equals "elements" with a corresponding ElementsView component. Either create a
new ElementsView placeholder component or use an existing appropriate component
for the elements surface. This ensures that selecting the Elements rail item
displays the correct content instead of duplicating the Design view.

In `@desktop/src/apps/MessagesApp.tsx`:
- Line 2761: The fallback logic for the topic field at line 2761 uses the
nullish coalescing operator `??` which only handles null and undefined values,
not empty strings. When currentChannel.topic is an empty string, it will not
fall back to currentChannel.description as intended, resulting in blank topic
text being rendered. Replace the nullish coalescing operator with a truthy check
(using the logical OR operator `||` or a ternary conditional) that treats both
null/undefined AND empty strings as falsy values, ensuring that empty topics
properly fall back to the description value.

In `@desktop/src/apps/MusicStudioApp.tsx`:
- Around line 56-68: The Export button in the MusicStudioApp component lacks
active-state semantics that other rail items provide. Add an aria-current
attribute to the Export button that sets it to "page" when view equals "export"
and is false or omitted otherwise. This will ensure assistive technologies can
properly announce the active navigation state, providing parity with the other
rail buttons like Studio, Compose, Sounds, and Mixer.

In `@desktop/src/apps/officesuite/WriteView.tsx`:
- Around line 113-115: The sample copy text in the WriteView component on lines
113-115 references outdated studio names "Images Studio and Game Studio" that do
not match the current studio set being described in the surrounding text which
mentions "Coding Studio," "Design," "Music," "App," and "Office" studios. Update
the text in this section to reference the correct, current studio names instead
of the stale ones to ensure consistency and avoid confusing users during demos
or testing.

In `@desktop/src/apps/StoreApp/index.tsx`:
- Around line 933-935: The optional catalog is fetched only once on component
mount, causing the state to become stale when users install or uninstall
optional apps during the session. Extract the fetch logic that retrieves the
optional catalog from the current useEffect into a separate refresh function,
then call this function not only during the initial mount but also after
optional app installations and uninstalls are completed. This ensures the
optionalCatalog state is refreshed in-session whenever the app list changes,
keeping the optional updates list and "up to date" empty state synchronized with
actual installations/uninstalls.
- Around line 1154-1176: The updatableOptional list in the updates tab is not
filtered by the active search query, causing optional update rows to display
even when the search produces no matches elsewhere. Apply the same search
filtering logic used for the main filtered updates list to the updatableOptional
filter. The updatableOptional.filter call on line 1155 should include an
additional condition that matches against the search query in the same way the
main updates list is filtered. Also apply the same fix to the optional update
section mentioned at lines 1182-1187 to ensure consistency across all optional
update displays in the Updates tab.

In `@README.md`:
- Line 224: The sentence in the README describes Coding, App, Design, Music, and
Office studios as "on the way," but this PR introduces them as optional
installable shells that are now available. Update the wording to reflect that
these studio apps are now available as optional installations rather than future
functionality, while keeping the clarification about App Studio being taOS's own
app builder.

In `@tests/userspace/test_broker.py`:
- Around line 6-7: The _store function violates Ruff E702 by having multiple
statements on a single line separated by semicolons. Split the single line into
three separate lines within the function body, placing the UserspaceDataStore
initialization, the await s.init() call, and the return statement each on their
own line.

In `@tests/userspace/test_data_store.py`:
- Line 7: Split the chained statements separated by semicolons into separate
lines. On line 7, split the UserspaceDataStore instantiation and the await
s.init() call into two distinct lines. Apply the same refactoring to line 22
where a similar multi-statement line exists with a semicolon separator. This
will satisfy the Ruff E702 rule and improve test readability.

In `@tinyagentos/routes/userspace_apps.py`:
- Around line 26-54: The constants _BUNDLE_CSP and _BUNDLE_CSP_FIRST_PARTY are
currently identical despite the detailed comment explaining intended relaxations
for first-party packages. Either consolidate these two constants into a single
_BUNDLE_CSP constant if they should be the same, or modify
_BUNDLE_CSP_FIRST_PARTY to apply the documented relaxations mentioned in the
comment above it (such as the connect-src and style-src adjustments) to
differentiate it from the community CSP policy.

---

Duplicate comments:
In `@tinyagentos/routes/userspace_apps.py`:
- Line 256: The `await request.json()` call in the broker endpoint lacks error
handling for malformed or invalid JSON in the request body. Wrap the line `body
= await request.json()` in a try/except block to catch JSON parsing exceptions
and return a controlled 400 Bad Request response instead of allowing the
exception to propagate as a 500 error. Follow the same pattern already
implemented in other endpoints in the codebase for consistency.

---

Nitpick comments:
In `@desktop/src/apps/MusicStudioApp.test.tsx`:
- Around line 63-79: Add two new test cases following the same pattern as the
existing "switches to Sounds view on rail click" test to verify the Mixer and
Export views. For each new test, call renderApp(), use fireEvent.click() to
click the respective rail button (Mixer and Export), then verify that the
aria-current attribute is set to "page" on the navigation button and assert that
the expected heading or content for that view is defined using
screen.getByRole() or screen.getByText(). This ensures complete coverage of all
rail navigation states.

In `@desktop/src/apps/ProjectsApp/AddAgentDialog.tsx`:
- Around line 60-72: The addExternal function's check for the submitting state
is vulnerable to race conditions on rapid clicks since state updates are
asynchronous, allowing multiple API calls to be issued before the state is
actually updated. Add a synchronous useRef guard that is checked and set
immediately at the start of the addExternal function before the submitting state
check. Create a ref variable that tracks whether a request is currently in
flight and synchronously set it to true before making the API call, then set it
back to false in the finally block. This synchronous guard will reliably prevent
redundant addNative calls regardless of how quickly the user clicks.

In `@tests/userspace/conftest.py`:
- Around line 55-58: The code silently defaults uid to an empty string when
find_user("admin") fails to find the user, which can hide broken fixture setup
and produce invalid session state. After the find_user call that retrieves the
record, add an assertion to ensure the record exists and is not None before
extracting the id value. This will cause the fixture to fail fast and clearly
indicate that admin user creation or lookup failed, rather than proceeding with
an invalid empty-string user_id to the create_session call.

In `@tests/userspace/test_immutability.py`:
- Around line 7-9: The test_native_app_type_rejected function in this file is a
duplicate of an existing test in tests/userspace/test_package.py that covers the
same functionality. Remove the entire test_native_app_type_rejected function
from the current file to eliminate the duplicate test coverage and reduce
maintenance burden.

In `@tests/userspace/test_seed.py`:
- Line 171: The test assertion in the test_seed.py file is hardcoding the
version string "1.0.0" which creates maintenance burden when the actual version
in tinyagentos/userspace/seed/welcome/manifest.yaml is updated. Instead of
hardcoding the version value in the assertion, dynamically load the version from
the manifest.yaml file and use that loaded value in the comparison. This way,
the test will automatically use the current version from the manifest without
requiring manual updates whenever the version is bumped.
- Around line 79-89: The installed_at timestamp assertion is vulnerable to false
passes due to second-level time resolution granularity. Mock or patch the system
time in the test to return controlled, deterministic timestamps so that the
first and second seed operations would produce different installed_at values if
an unintended reseed actually occurred. Apply this time mocking to both the
initial seeding operation before the installed_at_first assertion and the
subsequent seed_bundled_apps call, ensuring they use different mocked timestamps
to make the idempotency check reliable.

In `@tests/userspace/test_update_consent.py`:
- Around line 21-25: The assertion for body["new_permissions"] is too loose and
only checks that "app.memory" exists within the set, which doesn't catch if
previously granted permissions are incorrectly re-flagged as new. Replace the
membership check with an exact assertion that verifies new_permissions equals
exactly the expected set of permissions, ensuring only the truly new permission
"app.memory" is included and no previously granted permissions are incorrectly
present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a7090f3-2ef0-4de5-a646-0f2944ef3b83

📥 Commits

Reviewing files that changed from the base of the PR and between 59c296d and 57dfde0.

⛔ Files ignored due to path filters (1)
  • desktop/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (81)
  • .github/dependabot.yml
  • README.md
  • desktop/node_modules
  • desktop/src/apps/AppStudioApp.test.tsx
  • desktop/src/apps/AppStudioApp.tsx
  • desktop/src/apps/CodingStudioApp.test.tsx
  • desktop/src/apps/CodingStudioApp.tsx
  • desktop/src/apps/DesignStudioApp.test.tsx
  • desktop/src/apps/DesignStudioApp.tsx
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/apps/MusicStudioApp.test.tsx
  • desktop/src/apps/MusicStudioApp.tsx
  • desktop/src/apps/OfficeSuiteApp.test.tsx
  • desktop/src/apps/OfficeSuiteApp.tsx
  • desktop/src/apps/ProjectsApp/AddAgentDialog.tsx
  • desktop/src/apps/ProjectsApp/ProjectMembers.tsx
  • desktop/src/apps/SandboxedAppWindow.tsx
  • desktop/src/apps/SettingsApp/UpdatesPanel.test.tsx
  • desktop/src/apps/SettingsApp/UpdatesPanel.tsx
  • desktop/src/apps/StoreApp/StudiosView.test.tsx
  • desktop/src/apps/StoreApp/StudiosView.tsx
  • desktop/src/apps/StoreApp/index.tsx
  • desktop/src/apps/StoreApp/types.ts
  • desktop/src/apps/StoreApp/updates-optional.test.tsx
  • desktop/src/apps/__tests__/SandboxedAppWindow.test.tsx
  • desktop/src/apps/appstudio/BuildView.tsx
  • desktop/src/apps/appstudio/PublishView.tsx
  • desktop/src/apps/appstudio/TemplatesView.tsx
  • desktop/src/apps/chat/ChannelSettingsPanel.tsx
  • desktop/src/apps/codingstudio/BuildView.tsx
  • desktop/src/apps/codingstudio/PreviewView.tsx
  • desktop/src/apps/codingstudio/TemplatesView.tsx
  • desktop/src/apps/designstudio/DesignView.tsx
  • desktop/src/apps/designstudio/MagicView.tsx
  • desktop/src/apps/designstudio/TemplatesView.tsx
  • desktop/src/apps/musicstudio/ComposeView.tsx
  • desktop/src/apps/musicstudio/SoundsView.tsx
  • desktop/src/apps/musicstudio/StudioView.tsx
  • desktop/src/apps/officesuite/CalcView.tsx
  • desktop/src/apps/officesuite/SlidesView.tsx
  • desktop/src/apps/officesuite/WriteView.tsx
  • desktop/src/components/Launchpad.tsx
  • desktop/src/hooks/use-installed-userspace-apps.ts
  • desktop/src/lib/__tests__/userspace-apps.test.ts
  • desktop/src/lib/userspace-apps.ts
  • desktop/src/registry/app-registry.ts
  • docs/STATUS.md
  • tests/test_apps_installed.py
  • tests/test_docs_only_update.py
  • tests/userspace/__init__.py
  • tests/userspace/conftest.py
  • tests/userspace/test_broker.py
  • tests/userspace/test_broker_route.py
  • tests/userspace/test_data_store.py
  • tests/userspace/test_e2e.py
  • tests/userspace/test_immutability.py
  • tests/userspace/test_install_security.py
  • tests/userspace/test_package.py
  • tests/userspace/test_routes.py
  • tests/userspace/test_sdk_route.py
  • tests/userspace/test_seed.py
  • tests/userspace/test_store.py
  • tests/userspace/test_trust.py
  • tests/userspace/test_update_consent.py
  • tests/userspace/test_url_guard.py
  • tinyagentos/app.py
  • tinyagentos/auto_update.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/apps.py
  • tinyagentos/routes/settings.py
  • tinyagentos/routes/userspace_apps.py
  • tinyagentos/userspace/__init__.py
  • tinyagentos/userspace/broker.py
  • tinyagentos/userspace/data_store.py
  • tinyagentos/userspace/package.py
  • tinyagentos/userspace/sdk/taos-app-sdk.js
  • tinyagentos/userspace/seed.py
  • tinyagentos/userspace/seed/welcome/index.html
  • tinyagentos/userspace/seed/welcome/manifest.yaml
  • tinyagentos/userspace/store.py
  • tinyagentos/userspace/url_guard.py

Comment thread desktop/node_modules
@@ -0,0 +1 @@
/Volumes/NVMe/Users/jay/Development/tinyagentos/desktop/node_modules No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

CRITICAL: Remove the host-specific symlink and gitignore node_modules instead.

This symlink points to an absolute macOS-specific path (/Volumes/NVMe/Users/jay/Development/...) and will break checkouts on any other machine, CI/CD pipeline, or Linux deployment. This issue is already documented in the PR objectives ("REPO-HYGIENE BUG: desktop/node_modules is a tracked gitlink ... gitignore it") and in STATUS.md LATEST-22, but it remains unresolved in this PR.

Action required:

  1. Remove desktop/node_modules from git tracking: git rm --cached desktop/node_modules
  2. Ensure desktop/node_modules/ is in .gitignore
  3. Verify the repository builds cleanly with npm ci instead of relying on the symlink
🤖 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/node_modules` at line 1, The desktop/node_modules directory is
currently a tracked symbolic link pointing to an absolute macOS-specific path
that will break the repository on other machines and CI/CD environments. Remove
this symlink from git tracking by using git rm --cached on the
desktop/node_modules entry, then add desktop/node_modules/ to the .gitignore
file to prevent it from being tracked in the future. After making these changes,
verify that the project builds successfully by running npm ci in the desktop
directory to ensure dependencies are properly installed without relying on the
symlink.

Comment on lines +79 to +93
<div className="grid w-full max-w-[760px] grid-cols-3 gap-[14px]">
{RESULTS.map(({ label, gradient }) => (
<div
key={label}
className="relative cursor-pointer overflow-hidden rounded-[12px] border border-shell-border transition-all hover:-translate-y-[3px]"
style={{ aspectRatio: "0.8", background: gradient }}
>
<div
className="absolute bottom-0 left-0 right-0 px-[11px] py-[9px] text-[11px] text-white"
style={{ background: "linear-gradient(transparent, rgba(0,0,0,0.6))" }}
>
{label}
</div>
</div>
))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use semantic buttons for selectable result tiles.

Line 81 renders interactive tiles as div, so they are not keyboard-activatable. This blocks keyboard navigation on the main “pick a result” action.

Suggested fix
-        <div className="grid w-full max-w-[760px] grid-cols-3 gap-[14px]">
+        <div className="grid w-full max-w-[760px] grid-cols-3 gap-[14px]">
           {RESULTS.map(({ label, gradient }) => (
-            <div
+            <button
               key={label}
+              type="button"
+              aria-label={`Use layout: ${label}`}
               className="relative cursor-pointer overflow-hidden rounded-[12px] border border-shell-border transition-all hover:-translate-y-[3px]"
               style={{ aspectRatio: "0.8", background: gradient }}
             >
               <div
                 className="absolute bottom-0 left-0 right-0 px-[11px] py-[9px] text-[11px] text-white"
                 style={{ background: "linear-gradient(transparent, rgba(0,0,0,0.6))" }}
               >
                 {label}
               </div>
-            </div>
+            </button>
           ))}
         </div>
📝 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
<div className="grid w-full max-w-[760px] grid-cols-3 gap-[14px]">
{RESULTS.map(({ label, gradient }) => (
<div
key={label}
className="relative cursor-pointer overflow-hidden rounded-[12px] border border-shell-border transition-all hover:-translate-y-[3px]"
style={{ aspectRatio: "0.8", background: gradient }}
>
<div
className="absolute bottom-0 left-0 right-0 px-[11px] py-[9px] text-[11px] text-white"
style={{ background: "linear-gradient(transparent, rgba(0,0,0,0.6))" }}
>
{label}
</div>
</div>
))}
<div className="grid w-full max-w-[760px] grid-cols-3 gap-[14px]">
{RESULTS.map(({ label, gradient }) => (
<button
key={label}
type="button"
aria-label={`Use layout: ${label}`}
className="relative cursor-pointer overflow-hidden rounded-[12px] border border-shell-border transition-all hover:-translate-y-[3px]"
style={{ aspectRatio: "0.8", background: gradient }}
>
<div
className="absolute bottom-0 left-0 right-0 px-[11px] py-[9px] text-[11px] text-white"
style={{ background: "linear-gradient(transparent, rgba(0,0,0,0.6))" }}
>
{label}
</div>
</button>
))}
</div>
🤖 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/designstudio/MagicView.tsx` around lines 79 - 93, The
interactive result tiles are currently rendered as div elements instead of
semantic button elements, making them inaccessible for keyboard navigation.
Replace the div element that wraps each result in the RESULTS.map function (the
one with key={label} and the className containing "cursor-pointer") with a
button element. Preserve all existing className and style attributes, and ensure
the button maintains the same visual appearance and interactivity as the current
div implementation.

Comment on lines +30 to +52
<div
key={s.idx}
role="button"
tabIndex={0}
aria-label={`Slide ${s.idx}: ${s.label}`}
className="relative flex aspect-video cursor-pointer items-center justify-center overflow-hidden rounded-lg border px-1.5 text-center text-[10px] font-bold text-white"
style={{
background: s.bg,
borderColor:
s.idx === 1 ? "var(--color-accent, #8b92a3)" : "rgba(255,255,255,0.08)",
outline: s.idx === 1 ? "2px solid #a9b0c2" : undefined,
outlineOffset: s.idx === 1 ? 1 : undefined,
}}
>
<span
className="absolute left-1.5 top-1 text-[8px]"
style={{ color: "rgba(255,255,255,0.7)" }}
>
{s.idx}
</span>
{s.label}
</div>
))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace div role="button" thumbnails with real <button> elements.

Lines 30-52 create interactive controls that are not fully keyboard-operable (no Enter/Space handling). Use native buttons for built-in keyboard and assistive-tech behavior.

Suggested fix
-        {SLIDES.map((s) => (
-          <div
+        {SLIDES.map((s) => (
+          <button
             key={s.idx}
-            role="button"
-            tabIndex={0}
+            type="button"
             aria-label={`Slide ${s.idx}: ${s.label}`}
             className="relative flex aspect-video cursor-pointer items-center justify-center overflow-hidden rounded-lg border px-1.5 text-center text-[10px] font-bold text-white"
             style={{
               background: s.bg,
               borderColor:
                 s.idx === 1 ? "var(--color-accent, `#8b92a3`)" : "rgba(255,255,255,0.08)",
               outline: s.idx === 1 ? "2px solid `#a9b0c2`" : undefined,
               outlineOffset: s.idx === 1 ? 1 : undefined,
             }}
           >
             <span
               className="absolute left-1.5 top-1 text-[8px]"
               style={{ color: "rgba(255,255,255,0.7)" }}
             >
               {s.idx}
             </span>
             {s.label}
-          </div>
+          </button>
         ))}
📝 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
<div
key={s.idx}
role="button"
tabIndex={0}
aria-label={`Slide ${s.idx}: ${s.label}`}
className="relative flex aspect-video cursor-pointer items-center justify-center overflow-hidden rounded-lg border px-1.5 text-center text-[10px] font-bold text-white"
style={{
background: s.bg,
borderColor:
s.idx === 1 ? "var(--color-accent, #8b92a3)" : "rgba(255,255,255,0.08)",
outline: s.idx === 1 ? "2px solid #a9b0c2" : undefined,
outlineOffset: s.idx === 1 ? 1 : undefined,
}}
>
<span
className="absolute left-1.5 top-1 text-[8px]"
style={{ color: "rgba(255,255,255,0.7)" }}
>
{s.idx}
</span>
{s.label}
</div>
))}
<button
key={s.idx}
type="button"
aria-label={`Slide ${s.idx}: ${s.label}`}
className="relative flex aspect-video cursor-pointer items-center justify-center overflow-hidden rounded-lg border px-1.5 text-center text-[10px] font-bold text-white"
style={{
background: s.bg,
borderColor:
s.idx === 1 ? "var(--color-accent, `#8b92a3`)" : "rgba(255,255,255,0.08)",
outline: s.idx === 1 ? "2px solid `#a9b0c2`" : undefined,
outlineOffset: s.idx === 1 ? 1 : undefined,
}}
>
<span
className="absolute left-1.5 top-1 text-[8px]"
style={{ color: "rgba(255,255,255,0.7)" }}
>
{s.idx}
</span>
{s.label}
</button>
))}
🤖 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/officesuite/SlidesView.tsx` around lines 30 - 52, Replace
the div element that has role="button" with a native HTML button element. The
button should retain the key prop, tabIndex, aria-label, className, and style
attributes from the original div. Remove the role="button" attribute since it's
now redundant on a real button element. Keep the span child element that
displays the slide index and the s.label text content as-is. This will
automatically provide full keyboard support (Enter/Space handling) and better
accessibility without requiring manual event handling.

Comment on lines +107 to +143
<input
type="checkbox"
checked={!!member.can_edit_canvas}
onChange={async (e) => {
await canvasApi.setPermission(projectId, member.member_id, e.target.checked);
onRefresh();
onChanged();
}}
/>
<span className="text-xs">Can edit canvas</span>
</label>
)}
{!isExternal && (member.member_kind === "native" || member.member_kind === "clone") && (
<label
style={{ display: "inline-flex", alignItems: "center", gap: 6 }}
title="Lead agents see all messages in the project channel, even without being @mentioned."
>
<input
type="checkbox"
checked={!!member.is_lead}
aria-label={`Toggle lead for ${label}`}
onChange={async (e) => {
await projectsApi.members.setLead(projectId, member.member_id, e.target.checked);
onRefresh();
onChanged();
}}
/>
<span className="text-xs">Lead</span>
</label>
)}
<button
type="button"
onClick={async () => {
await projectsApi.members.remove(projectId, member.member_id);
onRefresh();
onChanged();
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify thrown API behavior + async handlers without local catches.
rg -nC3 'async function http|if \(!r.ok\)|throw new Error' desktop/src/lib/projects.ts
rg -nC2 'onChange=\{async|onClick=\{async' desktop/src/apps/ProjectsApp/ProjectMembers.tsx

Repository: jaylfc/taOS

Length of output: 1245


Add error handling and busy state to member action handlers.

The three async handlers at lines 111, 129, and 140 await API calls that throw on non-OK responses without catching errors. Unhandled rejections can leave the UI inconsistent and provide no feedback to the user. Additionally, rapid clicks can trigger duplicate requests.

Suggested fix
 function MemberRow({
@@
 }) {
+  const [rowBusy, setRowBusy] = useState(false);
+  const runMemberAction = async (op: () => Promise<unknown>) => {
+    if (rowBusy) return;
+    setRowBusy(true);
+    try {
+      await op();
+      onRefresh();
+      onChanged();
+    } finally {
+      setRowBusy(false);
+    }
+  };
+
   return (
@@
             <input
               type="checkbox"
               checked={!!member.can_edit_canvas}
-              onChange={async (e) => {
-                await canvasApi.setPermission(projectId, member.member_id, e.target.checked);
-                onRefresh();
-                onChanged();
-              }}
+              disabled={rowBusy}
+              onChange={(e) =>
+                void runMemberAction(() =>
+                  canvasApi.setPermission(projectId, member.member_id, e.target.checked),
+                )
+              }
             />
@@
             <input
               type="checkbox"
               checked={!!member.is_lead}
+              disabled={rowBusy}
               aria-label={`Toggle lead for ${label}`}
-              onChange={async (e) => {
-                await projectsApi.members.setLead(projectId, member.member_id, e.target.checked);
-                onRefresh();
-                onChanged();
-              }}
+              onChange={(e) =>
+                void runMemberAction(() =>
+                  projectsApi.members.setLead(projectId, member.member_id, e.target.checked),
+                )
+              }
             />
@@
         <button
           type="button"
-          onClick={async () => {
-            await projectsApi.members.remove(projectId, member.member_id);
-            onRefresh();
-            onChanged();
-          }}
+          disabled={rowBusy}
+          onClick={() =>
+            void runMemberAction(() =>
+              projectsApi.members.remove(projectId, member.member_id),
+            )
+          }
           className="text-xs text-red-400 hover:underline"
           aria-label={`Remove ${label}`}
         >
🤖 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/ProjectsApp/ProjectMembers.tsx` around lines 107 - 143, Add
error handling and busy state management to the three async handlers in the
member permission UI. For the "Can edit canvas" checkbox onChange handler
(calling canvasApi.setPermission), the "Lead" checkbox onChange handler (calling
projectsApi.members.setLead), and the remove button onClick handler (calling
projectsApi.members.remove), wrap each API call in try-catch blocks to handle
errors gracefully, add a loading/busy state variable to disable the input
elements during the API call, and display error feedback to the user when the
API call fails so the UI remains consistent and prevents duplicate requests from
rapid clicks.

import { useState, useEffect, useCallback, useRef } from "react";
import { Settings, RefreshCw, AlertCircle, Check, ChevronDown, ChevronRight } from "lucide-react";
import { Settings, RefreshCw, AlertCircle, Check, ChevronDown, ChevronRight, Package } from "lucide-react";
import * as LucideIcons from "lucide-react";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n desktop/src/apps/SettingsApp/UpdatesPanel.tsx | head -60

Repository: jaylfc/taOS

Length of output: 2205


🏁 Script executed:

# Check the rest of the file for AppIconGlyph usage and see what icon names are passed
cat -n desktop/src/apps/SettingsApp/UpdatesPanel.tsx | tail -n +50

Repository: jaylfc/taOS

Length of output: 23201


🏁 Script executed:

# Find the app registry file
fd -t f "app-registry" desktop/src/registry/

Repository: jaylfc/taOS

Length of output: 134


🏁 Script executed:

cat -n desktop/src/registry/app-registry.ts

Repository: jaylfc/taOS

Length of output: 20544


🏁 Script executed:

# Check if LucideIcons is used anywhere else in the file
rg "LucideIcons" desktop/src/apps/SettingsApp/UpdatesPanel.tsx

Repository: jaylfc/taOS

Length of output: 204


🏁 Script executed:

# Check if there are other files that use the same pattern (namespace import + dynamic lookup)
rg "import \* as.*from.*lucide-react" desktop/src --type ts --type tsx

Repository: jaylfc/taOS

Length of output: 83


🏁 Script executed:

# Check if there are other files that use the same pattern (namespace import + dynamic lookup)
rg "import \* as.*from.*lucide-react" desktop/src

Repository: jaylfc/taOS

Length of output: 725


🏁 Script executed:

# Check how other files use their namespace imports - looking for dynamic lookup pattern
rg "icons\[.*\]" desktop/src/components/LaunchpadIcon.tsx -A 2 -B 2

Repository: jaylfc/taOS

Length of output: 243


Replace namespace-loaded Lucide icons with explicit optional-app icon map.

Lines 3 and 41–49 use import * as LucideIcons with dynamic pascal-case lookup, forcing the entire icon library into the bundle even though only a fixed set of optional-app icons are ever needed. Replace with explicit named imports of the icons actually used by optional apps and a simple lookup map.

Proposed change
-import { Settings, RefreshCw, AlertCircle, Check, ChevronDown, ChevronRight, Package } from "lucide-react";
-import * as LucideIcons from "lucide-react";
+import {
+  Settings, RefreshCw, AlertCircle, Check, ChevronDown, ChevronRight, Package,
+  Code2, Palette, Music, Blocks, FileText, ScrollText, PlayCircle, Github, AtSign, Globe,
+  type LucideIcon,
+} from "lucide-react";
+
+const OPTIONAL_ICON_MAP: Record<string, LucideIcon> = {
+  "code-2": Code2,
+  palette: Palette,
+  music: Music,
+  blocks: Blocks,
+  "file-text": FileText,
+  "scroll-text": ScrollText,
+  "play-circle": PlayCircle,
+  github: Github,
+  "at-sign": AtSign,
+  globe: Globe,
+};

 function AppIconGlyph({ iconName, size = 16 }: { iconName: string; size?: number }) {
-  const pascal = iconName
-    .split("-")
-    .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
-    .join("");
-  const Glyph = (LucideIcons[pascal as keyof typeof LucideIcons] as LucideIcons.LucideIcon) ?? Package;
+  const Glyph = OPTIONAL_ICON_MAP[iconName] ?? Package;
   return <Glyph size={size} />;
 }
🤖 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/SettingsApp/UpdatesPanel.tsx` at line 3, The namespace
import of `lucide-react` in UpdatesPanel.tsx forces the entire icon library into
the bundle even though only specific optional-app icons are used. Replace the
`import * as LucideIcons` statement with explicit named imports of only the
icons actually referenced in the optional-app icon lookup logic (around lines
41-49), then create a simple object map that maps icon names to the imported
icon components, eliminating the need for dynamic pascal-case lookup via
LucideIcons[].

data = json.loads(row_json)
if where and any(data.get(k) != v for k, v in where.items()):
continue
out.append({"id": rid, **data})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent row payloads from overriding the authoritative database row id.

data can contain an "id" key and overwrite rid in the returned object, which breaks the table API contract for row identity.

Suggested fix
-            out.append({"id": rid, **data})
+            out.append({**data, "id": rid})
🤖 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/userspace/data_store.py` at line 72, The dictionary construction
in the append statement for the out list uses the wrong order of precedence,
allowing the "id" key from the data dictionary to override the authoritative rid
value. In the line where out.append is called with a dictionary containing "id":
rid and **data unpacked, reverse the order of precedence by placing the
authoritative rid assignment after the data unpacking so that {**data, "id":
rid} ensures rid always takes precedence over any "id" key that might exist in
the data dictionary.

Comment on lines +12 to +19
window.addEventListener("message", (e) => {
const m = e.data;
// Broker replies
if (m && m.taosAppReply != null && pending.has(m.taosAppReply)) {
const { resolve } = pending.get(m.taosAppReply);
pending.delete(m.taosAppReply);
resolve(m);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate message source before resolving RPC replies.

On Line 15, reply messages are accepted from any window. A different frame can spoof { taosAppReply: id } and resolve in-flight calls with attacker-controlled payloads.

Proposed fix
-  window.addEventListener("message", (e) => {
+  window.addEventListener("message", (e) => {
+    if (e.source !== parent) return;
     const m = e.data;
📝 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
window.addEventListener("message", (e) => {
const m = e.data;
// Broker replies
if (m && m.taosAppReply != null && pending.has(m.taosAppReply)) {
const { resolve } = pending.get(m.taosAppReply);
pending.delete(m.taosAppReply);
resolve(m);
}
window.addEventListener("message", (e) => {
if (e.source !== parent) return;
const m = e.data;
// Broker replies
if (m && m.taosAppReply != null && pending.has(m.taosAppReply)) {
const { resolve } = pending.get(m.taosAppReply);
pending.delete(m.taosAppReply);
resolve(m);
}
🤖 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/userspace/sdk/taos-app-sdk.js` around lines 12 - 19, The message
event listener in the anonymous callback does not validate the origin of
incoming messages before resolving pending RPC calls. Add an origin validation
check on the event object (e.origin) before processing the message and resolving
the promise. Only proceed with resolving the pending call when the message
originates from a trusted/expected origin, preventing spoofed messages from
different frames from hijacking in-flight RPC calls.

Comment on lines +29 to +34
function call(capability, args) {
const id = ++seq;
return new Promise((resolve) => {
pending.set(id, { resolve });
parent.postMessage({ taosApp: APP_ID, id, capability, args: 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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add timeout/reject cleanup for pending RPC calls.

On Lines 31-33, unresolved calls stay in pending forever if no broker response arrives. This leaks memory and leaves callers hanging.

Proposed fix
-  function call(capability, args) {
+  function call(capability, args) {
     const id = ++seq;
-    return new Promise((resolve) => {
-      pending.set(id, { resolve });
+    return new Promise((resolve, reject) => {
+      const timer = setTimeout(() => {
+        pending.delete(id);
+        reject(new Error(`taos_timeout:${capability}`));
+      }, 15000);
+      pending.set(id, {
+        resolve: (msg) => {
+          clearTimeout(timer);
+          resolve(msg);
+        },
+      });
       parent.postMessage({ taosApp: APP_ID, id, capability, args: args || {} }, "*");
     });
   }
🤖 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/userspace/sdk/taos-app-sdk.js` around lines 29 - 34, The call
function creates pending promises that are stored in the pending map but have no
timeout mechanism, causing memory leaks if broker responses never arrive. Add a
timeout handler within the call function that rejects the promise and removes
the entry from the pending map after a reasonable duration (such as 30 seconds).
Use setTimeout to trigger this cleanup, and ensure the timeout is cleared if the
response arrives before the timeout expires by storing the timeout ID alongside
the resolve function in the pending map entry.

Comment on lines +40 to +67
get: (k) => call("app.kv.get", { key: k }).then((r) => r.result),
set: (k, v) => call("app.kv.set", { key: k, value: v }),
delete: (k) => call("app.kv.delete", { key: k }),
keys: () => call("app.kv.keys", {}).then((r) => r.result),
},
table: {
insert: (t, row) => call("app.table.insert", { table: t, row }).then((r) => r.result),
query: (t, where) => call("app.table.query", { table: t, where }).then((r) => r.result),
delete: (t, id) => call("app.table.delete", { table: t, id }),
},
files: {
read: (p) => call("app.files.read", { path: p }).then((r) => r.result),
write: (p, content) => call("app.files.write", { path: p, content }),
},
notify: (title, body) => call("app.notify", { title, body }),
// gated -- resolve to {error:"permission_denied"} if not granted
net: { fetch: (url, opts) => call("app.net", { path: url, method: (opts && opts.method) || "GET", body: opts && opts.body, headers: opts && opts.headers }) },
backend: {
fetch: (path, opts) => call("app.net", {
path,
method: (opts && opts.method) || "GET",
body: opts && opts.body,
headers: opts && opts.headers,
}).then((r) => r.result),
},
agent: { ask: (name, message) => call("app.agent", { name, message }).then((r) => r.result) },
memory: { search: (q) => call("app.memory.search", { q }).then((r) => r.result) },
// Theme API -- populated only for first-party apps that receive taosTheme

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize broker error handling across SDK methods.

On Lines 40-67, some methods return r.result, others return raw envelopes. For gated capabilities (e.g., agent.ask, memory.search), { error: "permission_denied" } currently becomes undefined in callers, which hides permission failures.

Proposed fix
+  function unwrap(r) {
+    if (r && r.error) throw new Error(String(r.error));
+    return r ? r.result : undefined;
+  }
+
   window.taos = {
@@
-      get: (k) => call("app.kv.get", { key: k }).then((r) => r.result),
-      set: (k, v) => call("app.kv.set", { key: k, value: v }),
-      delete: (k) => call("app.kv.delete", { key: k }),
-      keys: () => call("app.kv.keys", {}).then((r) => r.result),
+      get: (k) => call("app.kv.get", { key: k }).then(unwrap),
+      set: (k, v) => call("app.kv.set", { key: k, value: v }).then(unwrap),
+      delete: (k) => call("app.kv.delete", { key: k }).then(unwrap),
+      keys: () => call("app.kv.keys", {}).then(unwrap),
@@
-      insert: (t, row) => call("app.table.insert", { table: t, row }).then((r) => r.result),
-      query: (t, where) => call("app.table.query", { table: t, where }).then((r) => r.result),
-      delete: (t, id) => call("app.table.delete", { table: t, id }),
+      insert: (t, row) => call("app.table.insert", { table: t, row }).then(unwrap),
+      query: (t, where) => call("app.table.query", { table: t, where }).then(unwrap),
+      delete: (t, id) => call("app.table.delete", { table: t, id }).then(unwrap),
@@
-      read: (p) => call("app.files.read", { path: p }).then((r) => r.result),
-      write: (p, content) => call("app.files.write", { path: p, content }),
+      read: (p) => call("app.files.read", { path: p }).then(unwrap),
+      write: (p, content) => call("app.files.write", { path: p, content }).then(unwrap),
@@
-    notify: (title, body) => call("app.notify", { title, body }),
+    notify: (title, body) => call("app.notify", { title, body }).then(unwrap),
@@
-    net: { fetch: (url, opts) => call("app.net", { path: url, method: (opts && opts.method) || "GET", body: opts && opts.body, headers: opts && opts.headers }) },
+    net: { fetch: (url, opts) => call("app.net", { path: url, method: (opts && opts.method) || "GET", body: opts && opts.body, headers: opts && opts.headers }).then(unwrap) },
@@
-      }).then((r) => r.result),
+      }).then(unwrap),
@@
-    agent: { ask: (name, message) => call("app.agent", { name, message }).then((r) => r.result) },
-    memory: { search: (q) => call("app.memory.search", { q }).then((r) => r.result) },
+    agent: { ask: (name, message) => call("app.agent", { name, message }).then(unwrap) },
+    memory: { search: (q) => call("app.memory.search", { q }).then(unwrap) },
🤖 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/userspace/sdk/taos-app-sdk.js` around lines 40 - 67, The gated
capability methods like agent.ask and memory.search currently extract and return
only r.result, which causes permission_denied error responses to become
undefined or hidden from callers. Normalize error handling by modifying the
gated capability methods (agent.ask at line 62 and memory.search at line 63) to
return the full response envelope instead of extracting r.result, ensuring that
error objects like {error: "permission_denied"} are properly visible to callers.
This should be consistent with how other methods handle responses to provide
uniform error detection across all SDK methods.

Comment on lines +68 to +73
# Re-seed (new app, version bump, or a non-first-party row claiming
# this id): remove any previously extracted files first so a smaller
# new version cannot inherit stale files from the old one, then extract.
shutil.rmtree(apps_root / app_id, ignore_errors=True)
zip_bytes = _build_zip_from_dir(app_dir)
extract_package(zip_bytes, apps_root)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard app_id path before rmtree to prevent out-of-root deletion.

extract_package() validates path safety, but it runs after deletion. A malformed manifest id can make shutil.rmtree(apps_root / app_id, ...) target outside apps_root.

Proposed fix
-            shutil.rmtree(apps_root / app_id, ignore_errors=True)
+            apps_root_resolved = Path(apps_root).resolve()
+            target_dir = (apps_root_resolved / app_id).resolve()
+            if target_dir == apps_root_resolved or not target_dir.is_relative_to(apps_root_resolved):
+                raise ValueError(f"unsafe bundled app id: {app_id!r}")
+            shutil.rmtree(target_dir, ignore_errors=True)
             zip_bytes = _build_zip_from_dir(app_dir)
             extract_package(zip_bytes, apps_root)
🤖 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/userspace/seed.py` around lines 68 - 73, The shutil.rmtree call
in the re-seed block uses app_id to construct the path without first validating
that it stays within apps_root, creating a security vulnerability where a
malicious manifest id could delete files outside the intended directory. Add
path validation before the shutil.rmtree call (before line 71) to ensure the
resolved path stays within apps_root, similar to how extract_package validates
paths. Check that resolving apps_root / app_id results in a path that is within
apps_root and reject or sanitize app_id values that would escape the directory.

@jaylfc jaylfc merged commit 203c9e2 into master Jun 17, 2026
10 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in TinyAgentOS Roadmap Jun 17, 2026
jaylfc added a commit that referenced this pull request Jun 17, 2026
…nomous build team + gate-cron running
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant