feat(desktop): Assistant Studio - a PA workspace#2103
Conversation
…t agent A new studio app where the user picks a registered agent to be their PA and works out of one hub. Left rail: Overview, Journal, Calendar/time, Tasks, Comms, Canvas, and a Deliverables (files/reports) area. The PA picker defaults to Hermes when present and persists the choice. Journal, Tasks, Calendar events and Deliverables persist locally per PA so switching PA swaps the whole workspace; Comms opens the live agent chat and Canvas points at the project canvas. MVP scope: self-contained, no new backend (localStorage-backed), so it is additive and safe. Accessible (labels, aria-current, keyboard add). Registered as an optional studio app. Backend wiring (real calendar, PA-scoped board/files) is a follow-up. tsc clean; frontend build passes.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAssistant Studio adds an optional multi-view workspace for selecting personal assistants and managing per-assistant journals, tasks, calendar events, and deliverables through localStorage, with communications, canvas, overview, and app registry integration. ChangesAssistant Studio workspace
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant AssistantStudioApp
participant AgentsAPI as /api/agents
participant localStorage
participant AgentChat
AssistantStudioApp->>AgentsAPI: Fetch available agents
AgentsAPI-->>AssistantStudioApp: Return agent list
AssistantStudioApp->>localStorage: Load selected agent data
localStorage-->>AssistantStudioApp: Return namespaced data
AssistantStudioApp->>AgentChat: Dispatch taos:open-agent-chat
Suggested reviewers: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
…2104) Assistant Studio (#2103) shipped in the frontend registry as optional but was not in the server-side optional-app catalog, so getLaunchableApps hid it (it is only shown once installed) and POST /api/apps/optional/assistant-studio/install returned 'not an optional app'. Add it to OPTIONAL_FRONTEND_APPS + the version / trust / provenance dicts, matching the other Creative Studios.
* fix(desktop): dialogs render above windows, and mint shows the URL and PIN (#2092) Two bugs on the same screen, both reported from live use. Window z-index was an unbounded counter: every open, focus, restore and recenter incremented nextZIndex forever, while portal overlays sit at a fixed z-[10001]. After enough focus switches in one session, windows rendered on top of modal dialogs. The stack is now renumbered to 1..N on each change, so window z stays far below the overlay layer regardless of session length, and relative order is preserved by sorting on the existing values first. This affected every portal overlay, not only the invite dialog. ProjectMembers closed the invite dialog in its onMinted handler, unmounting it before the result rendered. The invite URL and PIN are shown exactly once and cannot be recovered, so a successful mint looked like a silent failure. The parent now refreshes its member list only and the user closes the dialog once they have copied the credentials. * feat(agents): project_tasks_create scope so an external agent can author cards (#2098) An agent holding project_tasks could claim, close and comment on existing cards but never open one, so an approved grant bought nothing on that route. Rather than widen project_tasks, which is documented and tested as read plus lifecycle plus comments (Invariant 2 + 5) and would retroactively grant authoring to every agent already approved for it, authoring gets its own narrower scope that an owner opts into per agent. create_task now authorises through the same _authorize_task_actor as the other task routes, parameterised on scope, so existence-hiding 404s behave identically. The middleware allowlist admits POST .../tasks, which lets the token reach a handler that then verifies JWT, project binding and the narrower scope; project_tasks alone is still refused. Tests keep the original invariant (project_tasks alone cannot create) and add the halves that make it meaningful: the new scope DOES allow authoring, it is project-bound so a grant on A cannot create on B, and it does not widen member management. Authorship is attributed to the agent, not the project owner. * feat(library): item card component with thumbnail, status, artifacts, collection link (#2097) Add LibraryItemCard component per docs/design/library-app.md sections 2-4. Card shows thumbnail (or placeholder), title, kind badge, media duration, pipeline status per stage (jobs shape), artifact list (text, transcript, description, ocr) with preview, link-to-collection action, and a disabled Download stub until P3. Failure states are always visible -- no silent empties for missing thumbnails, pipeline stages, artifacts, or errors. Includes lib/library.ts with types and API client for the library store (items, artifacts, jobs) and 25 component tests covering pending, processing, ready, and error states. * feat(agents): enforce files_read/files_write so member agents can access project Files (#2100) * feat(agents): enforce files_read/files_write scopes so member agents can access project Files Project-files routes (/api/projects/{slug}/files*, mkdir, trash, stats) had no membership or scope gate and were absent from the agent middleware allowlist, so an agent token could not reach them at all while the files_read/files_write scopes existed but were never enforced. This wires them up, mirroring the canvas pattern: - _authorize_files_actor resolves slug -> project and authorizes a session owner/admin (unchanged) OR an agent holding files_read (reads) / files_write (writes) grant bound to that project. A token bound to another project, or an unknown slug, collapses into an existence-hiding 404; a missing scope is 403. - _AGENT_FILES_ROUTES added to auth_middleware so agent JWTs pass through to the routes, which verify the grant. - InviteAgentDialog offers files_read (default on) and files_write, so an owner can grant file access at invite time. Agents that are project members can now read the project's Files and add files via the API. Grant creation already grants these scopes generically, and membership is added via the always-on project_tasks scope. Adds tests/test_routes_project_files_agent_scope.py (10 cases covering read/write allow, missing-scope 403, cross-project 404, unknown-slug 404, session owner unchanged). * feat(agents): surface Files in the invite bundle + fix agent API-surface docs - build_connection_bundle now advertises the project Files endpoints and adds a Files capability section to the join guide when files_read/files_write are granted, plus task_create when project_tasks_create is granted, so a joining agent is told the Files API exists and how to reach it (slug-keyed paths). - docs/agent-coordination.md: drop the non-existent project_doc_review scope, add files_read/files_write, project_tasks_create, and decisions_write to the agent API-surface list, and correct the doc-gate note (it fires only on file add/delete, so it does not catch allowlist edits). - README: replace the understated read-only agent-surface sentence with the real scoped surface (tasks, canvas, files, decisions, a2a). Verified against VALID_SCOPES / _ALLOWED_SCOPES and the auth_middleware allowlist. Invite tests pass (36). * feat(providers): add Nous Portal as a cloud model provider (#2102) Nous Portal (Nous Research) is an OpenAI-compatible inference API serving the Hermes 4 family and frontier models. Wire it up as a first-class cloud provider so it can be added from the Providers app instead of a hand-configured openai-compatible endpoint. - providers/__init__.py: add 'nous' to ALL_TYPES + CLOUD_TYPES, and map it to the OpenAI LiteLLM prefix (api_base set explicitly, like kilocode). - routes/providers.py: default base URL https://inference-api.nousresearch.com/v1 and a seed model list (flagship Hermes models) for the case where /v1/models cannot be listed without a working credential. - backend_adapters.py: 'nous' uses the CloudAPIAdapter probe. - Frontend: add 'nous' to the cloud provider type lists and the Providers app metadata (label 'Nous Portal', default URL, description, key placeholder). Base URL and OpenAI-compatibility verified against Nous Portal docs. Backend provider suite passes (68); frontend tsc clean. * feat(desktop): Assistant Studio - a workspace for a personal-assistant agent (#2103) A new studio app where the user picks a registered agent to be their PA and works out of one hub. Left rail: Overview, Journal, Calendar/time, Tasks, Comms, Canvas, and a Deliverables (files/reports) area. The PA picker defaults to Hermes when present and persists the choice. Journal, Tasks, Calendar events and Deliverables persist locally per PA so switching PA swaps the whole workspace; Comms opens the live agent chat and Canvas points at the project canvas. MVP scope: self-contained, no new backend (localStorage-backed), so it is additive and safe. Accessible (labels, aria-current, keyboard add). Registered as an optional studio app. Backend wiring (real calendar, PA-scoped board/files) is a follow-up. tsc clean; frontend build passes. * feat(agents): request additional scopes for an existing agent identity (#1921) * feat(agents): request additional scopes for an existing agent identity Add a scope-request flow so an already-registered agent can gain more scope grants on its SAME canonical_id, instead of the auth-request flow which mints a new identity on approval (and 409s on an active-handle collision). Endpoints (in routes/agent_auth_requests.py): - POST /api/agents/registry/{cid}/scope-requests (create) - POST /api/agents/registry/{cid}/scope-requests/{id}/approve - POST /api/agents/registry/{cid}/scope-requests/{id}/deny Auth (security-critical): creation is gated to the agent's OWN registry bearer token (sub == canonical_id) OR the owning user / an admin, because the agent already holds credentials; an anonymous caller can never escalate an existing identity. The middleware allowlist exposes only the create path to a registry JWT; approve/deny are owner/admin only. Approval writes add_grant(cid, scope, project_id) per granted scope (idempotent via the UNIQUE key), never registers a second identity, and lets the admin narrow but not widen the requested scopes. decisions_read/decisions_write are grantable globally or per-project; project_tasks and canvas scopes still require an explicit project_id. Adds AgentScopeRequestsStore, a scope-agnostic check_agent_identity helper, and full route tests. VALID_SCOPES stays in sync with _ALLOWED_SCOPES. Fixes #1920 * fix(agents): fold scope-request approval security findings (#1921) Addresses the Kilo + CodeRabbit findings on the approve/create scope-request paths: - Major (project binding): approve_scope_request bound global-capable scopes (decisions_*) to effective_project, which fell back to the agent-named req.project_id when the operator gave no explicit project_id. Since an agent can self-request, that let a global scope bind to any project the operator never validated (cross-project escalation). Now grants bind ONLY to the operator's explicit body.project_id (None = global); the agent-named value is never a binding. - Atomicity + races: approve_scope_request wrote grants + membership before the set_decision flip with no lock, so concurrent approvals could double-grant. Wrapped the whole approval in the same per-request _get_approve_lock the consent path uses, with a pending re-check inside the lock (grant-before-flip is safe under the lock + idempotent add_grant). - Info leak: create_scope_request now authorizes BEFORE scope-vocabulary validation, so an unauthorized caller cannot probe whether a scope name is valid. Adds tests: global scope ignores an agent-supplied project_id (binds global), and create authorizes before vocab (403 not a 400 vocab leak). 16 tests pass. * fix(agents): take the per-request lock in deny_scope_request too (#1921) Kilo review: approve_scope_request now serializes concurrent approvals under _get_approve_lock; deny lacked the same lock, so a concurrent approve+deny of the same request was unserialized. Wrap the deny body in the same per-req lock with a pending re-check, matching approve and the sibling consent path. * fix(apps): register assistant-studio as an installable optional app (#2104) Assistant Studio (#2103) shipped in the frontend registry as optional but was not in the server-side optional-app catalog, so getLaunchableApps hid it (it is only shown once installed) and POST /api/apps/optional/assistant-studio/install returned 'not an optional app'. Add it to OPTIONAL_FRONTEND_APPS + the version / trust / provenance dicts, matching the other Creative Studios. * fix(shortcuts): resolve the container's real incus project (and start it) for terminal shortcuts (#2105) The agent terminal/TUI shortcuts opened an incus PTY with no --project flag, so incus used the client's default project (a per-user one like user-999). An agent whose container lives in a different project (e.g. a legacy container in 'default') failed with 'Failed to fetch instance taos-agent-<name> in project user-999: Instance not found', even though the container exists. Every other container op already resolves the real project via _resolve_container_project (--all-projects); the PTY path was the lone exception. _open_incus_pty now resolves the container's actual project and passes --project, and starts the container if it is stopped (incus exec fails on a non-running instance), so a dev-access shortcut works regardless of project or run state. Adds a sync _resolve_project_and_state_sync sibling to the async resolver. Tests: exec targets the resolved project + stopped container is started; a running container is not restarted. * test(scope-requests): add grant-enforcement E2E, tighten approve tolerance, add deny negative test (#2108) Three test gaps filled following PR #1921 merge: 1. E2E grant-enforcement test (test_approved_scope_grant_unlocks_route_e2e): agent self-requests decisions_write → admin approves → agent uses token on POST /api/decisions → assert 200. Proves the full grant-enforcement chain is wired end to end. Regression guard for issue #2095. 2. Tighten test_agent_cannot_approve_its_own_request: replace (401, 403) tolerance with exact 401. The middleware does not pass a registry JWT through to the approve handler (only the create path is allowlisted), so it falls to the session gate → 401 exactly. 3. Deny negative test (test_agent_cannot_deny_its_own_request): same auth model as approve — middleware does not allowlist the deny endpoint for registry JWTs, so an agent token on the deny endpoint falls through to the session gate → 401. Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com> * chore(deps): bump actions/setup-python from 6 to 7 (#2109) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the python-deps group with 2 updates (#2110) Updates the requirements on [matrix-nio](https://github.com/matrix-nio/matrix-nio) and [litellm[proxy]](https://github.com/BerriAI/litellm) to permit the latest version. Updates `matrix-nio` from 0.25.2 to 0.26.0 - [Changelog](https://github.com/matrix-nio/matrix-nio/blob/main/CHANGELOG.md) - [Commits](matrix-nio/matrix-nio@0.25.2...0.26.0) Updates `litellm[proxy]` to 1.93.0 - [Release notes](https://github.com/BerriAI/litellm/releases) - [Commits](BerriAI/litellm@v1.92.0...v1.93.0) --- updated-dependencies: - dependency-name: matrix-nio dependency-version: 0.26.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-deps - dependency-name: litellm[proxy] dependency-version: 1.93.0 dependency-type: direct:production dependency-group: python-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(release): v1.0.0-beta.44 version bump + changelog (#2121) * chore(release): v1.0.0-beta.44 version bump + changelog * docs(changelog): complete the beta.44 entry (dialog/mint fix, task-create scope) * test(desktop): add unit tests for AssistantStudioApp (#2116) * test(desktop): add unit tests for AssistantStudioApp * test(desktop): await the mount-time agents fetch in the first two Assistant Studio tests Qodo review: the first two tests rendered the component without awaiting its mount-time /api/agents fetch, so the resulting setState could land outside React Testing Library's act() and flake in stricter environments. The later tests in the same file already waited, so this was an inconsistency as much as a latent flake. Both now drain the fetch before asserting. * fix(library): wire up the unused source ingest option (#2117) * fix(library): wire up the unused source ingest option * fix(library): remove the unused source ingest option instead of sending it Review found that serializing source only moved the silent drop server-side: /api/library/ingest accepts file, url and title only, and LibraryStore has no source column (its source_url is already derived from the url). So the field was discarded either way, while now looking wired. The card offered wire-it-in or remove-it and I recommended wire-it-in without checking the backend, which was wrong. Removing it is the honest fix: no caller passes source, so nothing breaks, and a dead option no longer implies a feature that does not exist. Capturing real source metadata is a backend feature, not a client nit. * chore(deps): bump the spa-deps group in /desktop with 17 updates (#2111) Bumps the spa-deps group in /desktop with 17 updates: | Package | From | To | | --- | --- | --- | | [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.19` | `1.1.23` | | [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.20` | `2.1.24` | | [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.11` | `2.1.15` | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.3` | `2.3.7` | | [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.3.0` | `1.3.3` | | [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.3` | `1.3.7` | | [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.17` | `1.1.21` | | [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.12` | `1.2.16` | | [@tiptap/extension-link](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-link) | `3.28.0` | `3.29.0` | | [@tiptap/extension-underline](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-underline) | `3.28.0` | `3.29.0` | | [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) | `3.28.0` | `3.29.0` | | [@tiptap/react](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/react) | `3.28.0` | `3.29.0` | | [@tiptap/starter-kit](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/starter-kit) | `3.28.0` | `3.29.0` | | [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.2.7` | `19.2.8` | | [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.2.7` | `19.2.8` | | [@playwright/test](https://github.com/microsoft/playwright) | `1.61.1` | `1.62.0` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.3` | `6.0.4` | Updates `@radix-ui/react-dialog` from 1.1.19 to 1.1.23 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog) Updates `@radix-ui/react-dropdown-menu` from 2.1.20 to 2.1.24 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu) Updates `@radix-ui/react-label` from 2.1.11 to 2.1.15 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label) Updates `@radix-ui/react-select` from 2.3.3 to 2.3.7 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) Updates `@radix-ui/react-slot` from 1.3.0 to 1.3.3 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot) Updates `@radix-ui/react-switch` from 1.3.3 to 1.3.7 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch) Updates `@radix-ui/react-tabs` from 1.1.17 to 1.1.21 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs) Updates `@radix-ui/react-tooltip` from 1.2.12 to 1.2.16 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip) Updates `@tiptap/extension-link` from 3.28.0 to 3.29.0 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-link/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/extension-link) Updates `@tiptap/extension-underline` from 3.28.0 to 3.29.0 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-underline/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/extension-underline) Updates `@tiptap/pm` from 3.28.0 to 3.29.0 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/pm/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/pm) Updates `@tiptap/react` from 3.28.0 to 3.29.0 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/react) Updates `@tiptap/starter-kit` from 3.28.0 to 3.29.0 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/starter-kit/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.0/packages/starter-kit) Updates `react` from 19.2.7 to 19.2.8 - [Release notes](https://github.com/react/react/releases) - [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/react/react/commits/v19.2.8/packages/react) Updates `react-dom` from 19.2.7 to 19.2.8 - [Release notes](https://github.com/react/react/releases) - [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/react/react/commits/v19.2.8/packages/react-dom) Updates `@playwright/test` from 1.61.1 to 1.62.0 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](microsoft/playwright@v1.61.1...v1.62.0) Updates `@vitejs/plugin-react` from 6.0.3 to 6.0.4 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.4/packages/plugin-react) --- updated-dependencies: - dependency-name: "@radix-ui/react-dialog" dependency-version: 1.1.23 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-dropdown-menu" dependency-version: 2.1.24 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-label" dependency-version: 2.1.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-select" dependency-version: 2.3.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-slot" dependency-version: 1.3.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-switch" dependency-version: 1.3.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-tabs" dependency-version: 1.1.21 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-tooltip" dependency-version: 1.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@tiptap/extension-link" dependency-version: 3.29.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@tiptap/extension-underline" dependency-version: 3.29.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@tiptap/pm" dependency-version: 3.29.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@tiptap/react" dependency-version: 3.29.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@tiptap/starter-kit" dependency-version: 3.29.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: react dependency-version: 19.2.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: react-dom dependency-version: 19.2.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@playwright/test" dependency-version: 1.62.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@vitejs/plugin-react" dependency-version: 6.0.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: spa-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(agents): bind project_tasks_create and files scopes to a project on approval (#2127) * fix(agents): bind project_tasks_create and files scopes to a project on approval Review of the beta.44 promotion found that _SCOPE_PROJECT_SCOPES listed only project_tasks and the canvas scopes, so project_tasks_create, files_read and files_write could be approved with no project_id. The grant was then written global (project_id=None), and check_agent_scope_for_project only matches a grant bound to the project, so the operator believed they had granted access while the agent silently had none. Fails closed, but silently wrong is its own bug. Also replaces the em dashes in docs/agent-coordination.md with commas and colons per the house style, and records in project_files.py why the session path deliberately allows an unknown slug (lazily-created, slug-addressed files tree, documented by test_list_unknown_slug_returns_empty) while the agent path stays strict. 42 tests pass (project files, files agent scope, scope requests). * fix(projects): setting an agent as project lead now sets lead_member_id (#2113) add_agent_to_project wrote role='lead' on the member row but never called set_lead, which is the only writer of projects.lead_member_id. The member row is just a label; the pointer column is the actual lead. So the agent read as lead in the UI while every lead-gated check refused them. This is what happened to Hermes on taOSrabbit: role='lead', is_lead=0, lead_member_id NULL. Best-effort like the rest of that block, since the membership and grant already stand on their own. * fix(agents): one definition of which scopes require a project binding Qodo caught that the auth-request approval path still granted files_* and project_tasks_create globally. The project-scope set existed as three parallel copies (two function-local, one module-level), and the earlier fix only corrected the module-level one -- leaving the path an invite actually takes still writing those grants with project_id=None. check_agent_scope_for_project only matches a grant bound to that exact project, so a global grant never matches. The approval returns 200, the operator believes access was granted, and the agent silently has none. Now defined once and referenced everywhere; _SCOPE_* are plain aliases rather than rebuilt literals, since re-listing the members is how the copies drifted. Adds the regression test that was missing: nothing pinned this set, which is why three copies could disagree unnoticed. Checks alias identity (not equality) so a re-introduced copy fails even while it still happens to agree, asserts a single assignment per name in the module source, and asserts every project-bound scope is in VALID_SCOPES -- a typo there fails open, granting globally. * ci: raise the test timeout above the actual suite runtime (#2134) The cap was 45 min with a comment claiming 3.12/3.13 finish in ~16. Measured over the last 12 job records that is no longer true: 3.13 takes 31-41 min and 3.12 takes 38-41, so the cap sat roughly 4 minutes above the slowest normal run. Two of those 12 were killed mid-suite with nothing actually wrong, including the one gating #2127. A cap that close to the median does not catch hangs, it manufactures red PRs, and a timeout kill is indistinguishable from a real failure until you check the clock against timeout-minutes. That is the worst property a merge gate can have. 75 keeps a bound on a genuinely hung job while leaving real headroom. The suite growing from ~16 to ~40 min is its own problem and is filed separately; this stops it corrupting merge decisions in the meantime. * feat(feedback): add per-user 24h submission cap (#2131) * feat(feedback): add per-user 24h submission cap * fix(feedback): make the 24h cap atomic (Qodo) The cap did a SELECT COUNT then an INSERT as two awaited calls. Every sequential test passes and the limit still does not hold: concurrent requests all read the same count, all see room, and all insert. Verified against the pre-fix code, 8 racers with one slot left all got 201 and took a cap of 20 to 27. A user with a few tabs open trips this without trying, and a scripted client defeats it outright. Moved enforcement into the store as a single INSERT ... SELECT ... WHERE (SELECT COUNT(*) ...) < ?, so SQLite's write lock makes the check and the insert indivisible, and rowcount says which way it went. Adds the concurrency test that was missing, plus one asserting a rejected submission leaves no row behind: a partially-written reject would tighten the cap on every retry. * refactor(feedback): drop count_recent, orphaned by the atomic cap This PR added count_recent for the route to call before inserting. Moving the cap into create_within_cap left it with no callers anywhere in the tree, so it is dead on arrival rather than pre-existing code worth keeping. Removing it also removes the tempting wrong path: a future caller reaching for count_recent would reintroduce exactly the check-then-insert race the atomic statement exists to close. * test: replace always-true assert with issubclass check in test_installer_class_available (#1979) * fix(install): route LXC port allocation through centralized allocator Replace the standalone _find_free_port() in lxc_installer.py with allocate_host_port(app_id) from port_allocator.py so the centralized allocator is the single source of truth for all app host-port assignments. - Remove _find_free_port(), socket, and closing imports from lxc_installer - Import allocate_host_port instead of RESERVED_PORTS - Accumulate failed ports in exclude set across TOCTOU retry loop - Fix stale _docker_published_port docstring (DockerInstaller now maps {allocated_host_port}:{container_port}, not {p}:{p}) - Add test class verifying _find_free_port is gone and allocate_host_port is the only import Refs: #695 * test: strengthen allocator import assertion per Kilo suggestion Add assert not hasattr(mod, 'RESERVED_PORTS') to verify the old import is truly removed, not just that allocate_host_port is present. * test: replace always-true assert with issubclass check in test_installer_class_available --------- Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com> * feat(wallpaper): add Wallhaven proxy route + sectioned picker integration (#1902) * feat(wallpaper): add Wallhaven proxy route + browse-online picker section - Add wallhaven_api_key config field (env-only, never in repo) - Create GET /api/wallhaven/search proxy route to wallhaven.cc API - Keyless by default; optional X-API-Key header when WALLHAVEN_API_KEY set - Handle rate limiting (429), timeouts (504), and Wallhaven errors (502) - New WallhavenBrowser component: debounced search, thumbnail grid, pagination - Integrate WallhavenBrowser into WallpaperPicker as collapsible section - WallpaperPicker: "Browse online" toggle expands search UI, selecting a Wallhaven image applies it as a remote wallpaper - Backend tests (test_wallhaven.py, 12 tests) with respx mocking - Frontend tests (WallhavenBrowser.test.tsx, 8 tests; WallpaperPicker 14 tests) Fixes #864 * fix(wallpaper): escape CSS url, persist wallpaperIdByTheme, guard JSONResponse, validate categories/purity, move os import - Escape single-quotes and backslashes in remote wallpaper URLs to prevent CSS injection and render breakage (WallpaperPicker.tsx onSelect). - Route through a state updater that sets wallpaperIdByTheme, and include light/mobile/fallback variants so theme switch preserves remote wallpapers. - Use the received label as wallpaperOverlayText instead of discarding it. - Guard resp.json() with try/except ValueError and cap response at 1 MB (wallhaven.py:71). - Validate categories/purity with ^[01]{3}$ before forwarding to Wallhaven. - Move import os as _os from inside load_config to module top (config.py). * fix(wallpaper): harden CSS url() escaping — escape parens, strip control chars, validate http(s) scheme Kilo WARNING: CSS url() escaping was incomplete. Only backslashes and single-quotes were escaped, allowing ')' in query strings to prematurely terminate url('...') and inject CSS. Now also: - Escape '(' as %28 and ')' as %29 - Strip control characters (\x00-\x1f, \x7f) - Validate scheme is http(s) before proceeding Also removes a duplicate 'Browse online' section left behind during the sectioned-picker rebase — changes are low-risk and build/vitest pass. --------- Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com> * fix(gpu-arbiter): clamp drain_tick_seconds floor + remove double _signal_capacity wake (#1987) * feat(gpu-arbiter): event-driven admission wakeup replacing 2s poll tick (#1864 A3) Replace the hardcoded asyncio.sleep(2) in _process_queue with an asyncio.Event-based wake path. The drain loop now blocks on asyncio.wait_for(self._wake.wait(), timeout=drain_tick_seconds) so a reservation release or task completion immediately wakes the drain loop. The 2 s constant becomes the configurable fallback timeout (drain_tick_seconds, default 2.0). - __init__: new drain_tick_seconds param, self._wake Event - _signal_capacity(): new method — calls self._wake.set() - _process_queue: event-driven wait + clear, fallback timeout - _release_reservation: calls _signal_capacity on actual release - _run_gpu_task finally: calls _signal_capacity on completion - tests/test_gpu_arbiter_wakeup.py: 2 new tests * test_release_triggers_immediate_drain (60 s tick, < 0.5 s admit) * test_poll_tick_still_drains_without_signal (0.1 s tick) * fix(gpu-arbiter): clamp drain_tick_seconds floor + remove double _signal_capacity wake Kilo review on #1986: 1. Clamp drain_tick_seconds to >= 0.01 in __init__ to prevent asyncio.wait_for(timeout=0) ValueError crash in _process_queue. 2. Remove redundant _signal_capacity() in _run_gpu_task finally — _release_reservation already calls it at line 250, making the line-448 call a double-wake on every task completion. 32/32 GPU arbiter tests pass. --------- Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com> * fix(peer): address Kilo WARNINGs — os.environ leak, design doc, rate limiter docs (#2032) * fix(peer): address Kilo WARNINGs — align design doc, FIXME rate limiter - Design doc: outbound_token comment now matches code (plaintext; deferred to post-MVP) instead of misleading 'encrypted at rest'. - Rate limiter: add explicit FIXME for shared-store backing across workers, document per-worker aggregate limit semantics. Note: monkeypatch.setenv fixture fix was already applied in upstream merge of #2025, so this commit carries only the two remaining warnings. * fix(peer): address 5 Kilo findings — centralized auth, nonce replay, rate-limit LRU, prune commit, token docs Fix #1 (WARNING): Document outbound_token plaintext threat model in contacts_store schema comment — token must be presented on outbound requests; at-rest encryption deferred to post-MVP. Fix #2 (WARNING): Centralized /api/peer/ authentication via router-level _peer_auth_dep dependency. Previously EXEMPT_PREFIXES bypassed all auth middleware and correctness depended on every route calling _authenticate_peer. Now every route under /api/peer/ gets bearer-auth automatically; route handlers read contact_id from request.state. Fix #3 (SUGGESTION): Commit the opportunistic nonce prune in its own transaction so a replay (IntegrityError) rollback does not undo it. Fix #4 (SUGGESTION): Add record_nonce(…, kind='ack') to /api/peer/ack for replay protection. A replayed ack now returns 409 Conflict, matching the /inbox and /chat contract. Fix #5 (SUGGESTION): Add LRU fallback eviction to the rate limiter. When all 2000+ entries have active windows (no expired entries to sweep), the oldest entry is evicted to prevent unbounded dict growth under sustained load. --------- Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com> * fix(notifications): show approve/deny for scope-request notifications in bell (#2107) * fix(notifications): show approve/deny buttons for agent_scope_requests in bell and toast PR #1921 (agent scope-requests) emits notifications with source 'agent_scope_requests' but NotificationCentre and NotificationToast only rendered ConsentActions for source === 'auth_requests'. Scope- request approve/deny was invisible in the UI. - Branch source check in NotificationCentre and NotificationToast to also accept 'agent_scope_requests' - ConsentActions now accepts optional source + canonicalId props and routes approve/deny to the scope-request endpoints when source is 'agent_scope_requests' (/api/agents/registry/{id}/scope-requests/...) - consentPayload() extracts canonical_id from notification data for the scope-request endpoint path - Backward compatible: source defaults to 'auth_requests' for existing callers (DecisionsApp, tests) Ref: #1921 * fix: guard missing canonicalId for scope-request consent actions Per CodeRabbit review: the canonicalId ?? requestId fallback could produce malformed URLs like /registry/{requestId}/scope-requests/... when canonical_id is missing from the notification data. Now explicitly rejects scope-request approve/deny with a clear error when the canonicalId is absent. --------- Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com> * fix(csrf): merge CSRF token into effectiveInit for Request inputs (bot-fix for #1991) (#1999) * fix(csrf): merge CSRF token into effectiveInit for Request inputs, not rebuild - Compute effective method from init?.method || input.method - Merge token into effectiveInit headers instead of rebuilding Request (avoids body stream consumption and respects init-provided headers/method) - Update test to inspect init.headers instead of Request.headers Addresses Kilo finding (Request inputs excluded from CSRF) and CodeRabbit finding (broken token injection when init overrides method/headers + body stream consumption). Refs: PR #1991 * fix(csrf): merge Request headers with init headers instead of choosing one Kilo's WARNING on this PR was correct. `init?.headers || input.headers` picks one source, so whenever a caller supplied headers via BOTH the Request and the init, the Request's own headers were discarded entirely: fetch(new Request(url, {headers: {Authorization}}), {method, headers}) lost the Authorization header. init also wins at the fetch layer for a Request plus init, so nothing downstream restored it. Now both are merged, init winning on conflict, which matches how fetch itself resolves the two. The regression test is verified to fail on the pre-fix code, and it exposes the bug as slightly worse than reported: with the old line, that call lost the CSRF token too, so the header this PR exists to attach was itself dropped in exactly the case it was being extended to cover. Test assertions live inside the existing it() block on purpose: installAuthGuard has a module-level `installed` flag, so a second install in a fresh it() is a no-op and window.fetch would be the bare spy rather than the wrapper. I lost time to that before spotting it, hence the note. --------- Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com> Co-authored-by: jaylfc <jaylfc25@gmail.com> * fix(canvas): make the .tldr export a file tldraw can actually open (#2133) * fix(canvas): make the .tldr export a file tldraw can actually open The .tldr snapshot is the data-recovery escape hatch for #2132: it is what a user falls back on to get their canvas out and open it elsewhere. It did not work, and nothing tested that it did. Two independent faults, both verified against tldraw 4.5.12 rather than assumed: 1. Wrong container entirely. We emitted {schema, store{}}, the shape of an in-memory store snapshot. A .tldr FILE is {tldrawFileFormatVersion, schema, records[]}. tldraw's tldrawFileValidator requires all three, so parseTldrawJsonFile threw and returned notATldrawFile -- a flat refusal to open, before it ever looked at the content. The declared schemaVersion 2 also lacked the required sequences dict. 2. Shapes stock tldraw cannot render. note/link/image were typed taos-note / taos-link / taos-image, our own shape utils. Nobody else's tldraw has them. On the live Pi that is 55 of 64 live elements. The rest were geo shapes carrying taos_* keys in props, which tldraw rejects as unknown props. Fixed by emitting the real envelope with a serialized schema captured from tldraw, mapping every kind onto native note/text/geo shapes, and moving taOS provenance into meta, which tldraw carries through untouched. Deliberately does NOT pass through a user_shape's literal tldraw_shape blob, despite that being the highest-fidelity option. One invalid record makes tldraw reject the WHOLE file, so a single stale blob would cost the user their entire board in the one file whose only job is recovery. That risk is concrete here: we declare a fixed schema constant, so tldraw runs no migration on a blob written by an older version. The lossless copy belongs in the canonical JSON export instead. The raw blob stays untouched in the DB payload either way. Index keys are generated properly rather than hardcoded to "a1": tldraw validates them, so "a10" is rejected outright (a fraction may not end in 0), and reusing one key discards z-order. Also sets Content-Disposition, without which the browser renders the JSON inline and the user never gets a file. Verified on real data, not just fixtures: all three live Pi boards exported and loaded into a stock tldraw with every shape intact (22/22, 25/25, 17/17). * test(canvas): update the two tests that pinned the old .tldr shape Both asserted "store" in body, which encoded the bug this branch fixes: the in-memory {schema, store{}} store-snapshot shape rather than the {tldrawFileFormatVersion, schema, records[]} file envelope tldraw actually accepts. They now assert the file format and read shapes out of records. My miss: these live under tests/projects/ and I only ran the files I had touched, so CI found them instead of me. Swept the tree for any other consumer of the old shape and there are none. * fix(canvas): make every field read in the .tldr export non-fatal CodeRabbit and Qodo independently caught the same bug, and they were right. _element_text did payload.get(...), but the `or {}` guard only catches falsy payloads: a truthy non-dict (a list or bare string decoded from the DB) reached .get() and raised AttributeError. el["id"], el["x"], el["kind"] could equally raise KeyError on an absent column. Any of those aborts _build_tldraw_snapshot for the WHOLE project. This is the recovery export, so it runs precisely on the boards whose rows are already ragged, and the failure mode was that one odd row costs the user every other shape they were trying to rescue. That directly contradicts the rule this file already states, which makes it a bug in the code rather than in the intent. Every read now degrades to a safe default instead of raising. A string payload becomes the shape's label, since if that is all the row holds it is the most useful thing to show. _num_or excludes bool deliberately: bool is an int subclass, so True would otherwise sail through as a width of 1.0 and silently misplace a shape rather than fall back. Verified against tldraw's real parser, not just unit tests: the live Pi board with malformed rows spliced in exports 67 elements and loads all 67 shapes in a stock tldraw. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: hognek <hognek@gmail.com> Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
What
A new Assistant Studio app: the user picks a registered agent to be their personal assistant (PA), then works out of one hub. For now Jay will invite @Hermes as the PA.
Sections (left rail)
Notes
/api/agents, defaults to Hermes when present, and persists the choice.aria-current, keyboard add.tsc clean;
npm run buildpasses locally.Summary by CodeRabbit