feat(enterprise): Client Portal — My Agents roster + chat (trinity-enterprise#78)#1476
Merged
Conversation
…rprise#78) Add the first client-facing Client Portal surface to the OSS bundle, gated by the `client_portal` entitlement (backing module in trinity-enterprise#91): - `views/enterprise/ClientPortal.vue` — a card grid of the agents shared with the signed-in email (avatar or deterministic initials tile, owner, shared date; a disabled "Chat — soon" affordance signals the next slice). Loading / empty / error states; dark-mode; falls back to initials if an avatar URL fails to load. - Route `/enterprise/client-portal` gated `meta.requiresEntitlement: 'client_portal'` (302s to the enterprise catalogue when unentitled; 404 from the backend in OSS-only builds). - `stores/clientPortal.js` — domain store; `fetchRoster()` over the gated `/api/enterprise/client-portal/my-agents` endpoint (Invariant #7: API via store, not the view). - Enterprise catalogue card in `views/enterprise/Index.vue`. The Vue ships in the OSS bundle (no IP) but stays hidden until the enterprise module registers `client_portal` — safe to land independently of the submodule bump. Related to trinity-enterprise#78. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nterprise#78)
Make the roster cards actionable: a "Chat" button opens a slide-over chat drawer
for that agent. Second Client Portal slice, on top of the "My Agents" roster.
- `views/enterprise/PortalChat.vue` — slide-over chat panel (user/assistant
bubbles, typing indicator, textarea composer, Enter-to-send). Assistant
markdown rendered via `utils/markdown.js` (DOMPurify, Invariant H-005).
- `ClientPortal.vue` — the card "Chat — soon" placeholder is now a live "Chat"
button that opens the drawer for the selected agent.
No new backend: chat reuses the existing OSS `POST /api/agents/{name}/chat`
(via `agentsStore.sendChatMessage`), which already authorizes a user whose email
is on the agent's `agent_sharing` allow-list — exactly the roster set. The portal
remains the entitlement-gated surface; the chat capability itself is the OSS one
a shared user already has.
Verified live (PG): as the shared user, chatting a rostered agent through the
portal's endpoint returns a real agent reply. SFCs compile clean.
Related to trinity-enterprise#78.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-in page (trinity-enterprise#78) The Client Portal's real client identity: a verified email, not a platform account. OSS owns the edition-agnostic primitive + the security fence; the enterprise module (trinity-enterprise#91) mints the token after code verification. OSS (`dependencies.py`) — mirrors the MFA-challenge-token pattern: - `create_portal_session_token(email)` / `decode_portal_session(token)` — a scope=`portal_session` JWT carrying only the email (no `sub`, no users row). - Fence: `get_current_user` and `decode_token` reject a `portal_session` token, so it authenticates NOTHING on the platform — only the entitled portal endpoints accept it (via the module's `get_portal_identity`). Verified live: 401 on `/api/agents` and `/api/settings`, 200 only on the portal roster. Frontend — public client surface: - `views/Portal.vue` at `/portal` (standalone, no NavBar, no `requiresAuth`): email → 6-digit code → roster of the agents shared with that email. A client signs in with no platform account. - `stores/clientPortal.js`: `requestCode` / `verifyCode` / `signOut`; the portal token is persisted (localStorage) and used as the auth header for the portal endpoints, falling back to platform login for operator preview. A 401 drops the token back to the sign-in form. Chat over a portal session is the next slice (the OSS chat endpoint fences the portal token by design); the roster card shows "Chat — soon" in the public view. Related to trinity-enterprise#78. Depends on trinity-enterprise#91 (mint + endpoints). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vybe
approved these changes
Jul 6, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
/validate-pr — APPROVE
Validated:
- CI fully green (3-seed pytest head+base, regression diff, schema-parity, CodeQL, prod-image-smoke, non-root)
- Security sweep on diff: no secrets/keys/emails/IPs; no .env or credential files
- Auth fence (
dependencies.py): portal-scoped token rejected in bothdecode_tokenandget_current_user(mirrors MFA_CHALLENGE_SCOPE);create_access_tokenmintsjtisodecode_portal_session's revocation check is live; token carries nosubso it can't resolve to a platform principal even without the fence - Enterprise ordering verified: the module at the #1477 bump SHA (f1bafee) does NOT import the new portal primitives, so this lands safely in either order; the portal auth endpoints 404 until the next enterprise slice — the /portal page's sign-in error state covers that window by design
- H-005: PortalChat renders assistant markdown via utils/markdown.js (DOMPurify) ✓
- Store uses the direct-axios pattern consistent with the 10 existing stores ✓
Non-blocking notes:
⚠️ Thedependencies.pyfence ships without an in-diff unit test (mint is unreachable until the enterprise endpoint lands — suggest the fence regression test rides that enterprise PR or a follow-up)⚠️ Related to trinity-enterprise#78is a non-closing ref — correct for an epic with more slices, but remember the manual status bump on ent#78 tracking- Suggestion: architecture.md auth section could gain a one-liner on the OSS portal-session primitive (core-primitive + enterprise-knob pattern, like #995) with the completing slice
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.
Showable Client Portal slices (epic #78), gated by
client_portal(backend module in trinity-enterprise#91). OSS-bundle Vue, hidden in OSS/unentitled builds viarequiresEntitlement.Slice 1 — "My Agents" roster
views/enterprise/ClientPortal.vue— card grid of agents shared with the signed-in email (avatar or deterministic initials tile, owner, shared date). Loading/empty/error states, dark mode, avatar→initials fallback./enterprise/client-portal(meta.requiresEntitlement: 'client_portal'),stores/clientPortal.jsfetchRoster(), catalogue card inviews/enterprise/Index.vue.Slice 2 — chat with a rostered agent
views/enterprise/PortalChat.vue— slide-over chat drawer (user/assistant bubbles, typing indicator, Enter-to-send). Assistant markdown viautils/markdown.js(DOMPurify, H-005).POST /api/agents/{name}/chat(agentsStore.sendChatMessage), already authorized for a user whose email is on the agent'sagent_sharinglist — the roster set. Portal stays the gated surface; chat is the OSS capability a shared user already has.Identity (both slices)
Uses platform email login as the client identity. The true verified-email portal session (a client is not a
usersrow — #78's hard line) is a later slice, as are documents + voice.Testing (live PG instance, running stack)
viewer@example.comsurfaces exactly those agents (owner + date); email-scoped; excludes deleted/system. 11/11 enterprise unit tests.client_portalentitled);/apiproxy reaches backend.Depends on trinity-enterprise#91 (endpoint + entitlement). Safe to land first — hidden until the module registers
client_portal.Related to trinity-enterprise#78.
🤖 Generated with Claude Code