fix(security): operator-queue access control — scope items to accessible agents (#470) - #472
Merged
Merged
Conversation
…ow card Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ble agents (#470) Fixes High-severity broken access control (pentest finding 3.1.3): all six operator-queue endpoints now enforce user-scoped filtering. - list + stats: pass accessible_agent_names set to DB layer; empty set short-circuits before any SQL executes (prevents IN () syntax error) - get / respond / cancel: 403 if item's agent_name not in accessible set - /agents/{name}: 403 if agent_name not in accessible set - Admin bypass: role==admin returns None (no filter), sees all items - All five stats sub-queries (by_status/type/priority/agent/avg/today) apply the same access filter so by_agent never leaks foreign agent names - 8 new isolation tests in TestOperatorQueueAccessControl covering all endpoints for non-admin and admin regression Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 tasks
vybe
pushed a commit
that referenced
this pull request
Sep 6, 2026
…s its rail (#2540, ent#474) (#2543) Two operator rulings from the 2026-09-06 Workspace review, one PR because the second is built under the first. #2540 — the loading rule. Design-system principle 12 is amended: the scanline beam + wipe-in reveal is the CHART-loading motion only; every other first load — pages, panels, lists, message threads — is a skeleton placeholder keyed on "no data yet". The three Workspace zones #2163 wrapped in `ScanlineReveal` (the stage in Portal.vue, the thread in PortalConversation.vue, the hint zone in PortalBriefing.vue) render `PortalSkeleton` (stage / thread / briefing) instead, gated on `stage.state === 'loading'`, `!historyLoaded` and `zone.state === 'pending'` — never a bare `<x>.loading` path, which the #1927 ratchet counts as a bare gate. The `reveal` verdict the zones computed for the primitive is gone. The footprint (`min-h`, `max-w`) moves onto a wrapper both faces sit inside, so the swap never shifts. `tests/unit/portalLoadingTreatment.spec.js` pins the `ScanlineReveal` importer set as an allowlist: three chart consumers plus the two pre-ruling non-chart holdovers (LibrarySkillsSection, FinishSetupCard), recorded on #1921 — whose sweep is re-pointed by comment: bespoke spinners on non-chart surfaces become skeletons, not scanlines. The doc names the canonical forms (`SkeletonLoader.vue`, or content-shaped) and the recipe; `/audit-design-system` reads the amended doc without a skill change. ent#474 — the rail shell, to the approved design pass (48px collapsed / `w-96` open, `trinity-workspace-rail`, mobile strip + sheet, two signal shapes). Scope is the operator's own split: the three-column layout, the tab contract, the collapsed signal, Work docked EMPTY — its content is #457's and the re-homing of loops / canvas / files is #472's second child; the sidebar, thread tab strip, top band, Agent-details panel and drop target of the approved conversation page are later steps of the same build, and #492 lands the grid variables the rail's widths then follow. - `portalRail.js` is the contract: a tab declares door (platform / audience / agent), participant scope, empty state and signal shape; `visibleTabs` is the ONE gate for render AND mount, so a tab whose door the session fails has no icon, no label and no mounted body — nothing it would fetch is ever requested (the per-door test; an external client sees no rail until an audience tab docks). An unknown door fails closed. - `PortalRail.vue` is a sibling of <main>, outside `convKey`, so a chat switch remounts the conversation and never the rail; visibility keys on the route and the stage verdict, never on data still arriving. Collapsed by default; open/collapsed + active tab persist under one key, read before first paint. - The Work signal is DERIVED — from the conversation's in-flight `sending` (1:1) and the room's server-reported `working` list — reset on every chat switch and cleared on unmount, so it structurally cannot stick. - A room groups by participant over `portalLoopUtils.byAgent`, absence visible; `1 running` is a `BaseBadge`. `OverflowTabs` gains an optional per-tab `signal` dot, measured in its mirror row. - Mobile: `PortalRailStrip` above the composer through a `#rail-strip` slot on both conversations, opening the same component as a bottom sheet (Esc closes). Verified live on the Docker frontend: collapsed → open → persisted across reload; light and dark; mobile strip → sheet → Escape; the stage skeleton under a slowed roster; no console errors. Unit suite 1889/1889; both ratchets flat (no bare gate added, no touched file's raw-gray count grew). Fixes #2540 Part of Abilityai/trinity-enterprise#474 Claude-Session: https://claude.ai/code/session_01FgAncnTAbgjJS3EGwuEtd4 Co-authored-by: sim <sim@example.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
vybe
pushed a commit
that referenced
this pull request
Sep 6, 2026
…p and the drawer are gone (Abilityai/trinity-enterprise#475) (#2544) Slice 2 of the conversation rail (ent#472): the three placements #472 found move into the ent#474 frame. The loops strip above the composer becomes the Loops tab, the Files slide-over becomes the Files tab (PortalFilesPanel deleted), and the agent canvas gets its conversation-side placement as the Canvas tab — the same CanvasPanel and client-portal store the Workspace agent page uses, audience-narrowed by the ent#438 ruling. One owner for what the tabs read (composables/usePortalRailFeeds.js): it feeds stores/portalLoops.js and the new stores/portalRailFeeds.js off the rail's door gate (feedsFor over visibleTabs) and only while the rail is on screen, so a session that fails a tab's door never fetches its data, a deep link to an unreachable agent issues no request, and the collapsed rail can signal with no body mounted. Loops = live (the store's active loops); Canvas and Files = "updated since last view" — newest server timestamp per participant vs a seen marker (epoch compare via parseUTC, Invariant #16), persisted under trinity-workspace-rail-seen. Refresh is push/event-driven (turn end, room idle, loop events, terminal agent_activity — debounced), on tab open and after an upload; no timer while idle. The header paperclip opens the rail on Files (column at sm+, sheet below, never persisting open from a phone). "Ask for a canvas" pre-fills the composer in a conversation AND a room; never sends. CanvasPanel re-reads blocks when the selected canvas's updated_at moves, so a lit dot never opens onto stale content. The Files spinner is replaced by a rail skeleton keyed on the feed's verdict (AC 6 as amended 2026-09-06); LoadFailed / InlineError on failure. Review finding folded in: no rail.reset() in the shell's chat-switch watch — watchers run in creation order and the owner had already re-scoped both stores, so the reset wiped the new chat's data (docs/memory/learnings.md). Tests: portalRail.spec (four-tab registry, doors → fetch, timestamps as instants, updatedSignal, seen markers, feedView, railOpenPlan, removed placements, owner wiring), new portalRailFeeds.spec (store + owner under Pinia), portalLoops.spec re-pinned to the one owner, loading-treatment guard extended, loading-gate baseline regenerated. Live pass on the Docker frontend: four tabs, ask → prefill, paperclip → rail, room grouping, mobile strip → sheet → Esc, zero console errors. Fixes Abilityai/trinity-enterprise#475 Claude-Session: https://claude.ai/code/session_01PBTEMcpMsvPC5U4QnKevLC Co-authored-by: sim <sim@example.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GET /api/operator-queueand related endpoints were returning items for all agents on the platform regardless of requester ownership/agents/{name}Changes
src/backend/db/operator_queue.py—list_items()andget_stats()acceptaccessible_agent_names: Optional[Set[str]]; empty set short-circuits before SQL executes (preventsIN ()syntax error); all 5 stats sub-queries apply the filter soby_agentnever leaks foreign agent namessrc/backend/routers/operator_queue.py—_accessible_set()helper computes user's allowed agents viadb.get_accessible_agent_names();_assert_agent_accessible()raises 403 on unauthorized access; applied to all 6 endpointssrc/backend/database.py—get_operator_queue_stats()passes**kwargsthrough to support the new paramtests/test_operator_queue.py— 8 new tests inTestOperatorQueueAccessControl: non-admin list/get/respond/cancel/agents/stats isolation + admin regressionTest Plan
pytest tests/test_operator_queue.py -vby_agentcontains no foreign agentsCloses #470
🤖 Generated with Claude Code