Skip to content

feat(enterprise): Client Portal — My Agents roster + chat (trinity-enterprise#78)#1476

Merged
vybe merged 3 commits into
devfrom
feature/78-client-portal-roster
Jul 6, 2026
Merged

feat(enterprise): Client Portal — My Agents roster + chat (trinity-enterprise#78)#1476
vybe merged 3 commits into
devfrom
feature/78-client-portal-roster

Conversation

@dolho

@dolho dolho commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Showable Client Portal slices (epic #78), gated by client_portal (backend module in trinity-enterprise#91). OSS-bundle Vue, hidden in OSS/unentitled builds via requiresEntitlement.

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.
  • Route /enterprise/client-portal (meta.requiresEntitlement: 'client_portal'), stores/clientPortal.js fetchRoster(), catalogue card in views/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 via utils/markdown.js (DOMPurify, H-005).
  • Roster card "Chat" button opens the drawer.
  • No new backend: reuses OSS POST /api/agents/{name}/chat (agentsStore.sendChatMessage), already authorized for a user whose email is on the agent's agent_sharing list — 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 users row — #78's hard line) is a later slice, as are documents + voice.

Testing (live PG instance, running stack)

  • Roster: a share to viewer@example.com surfaces exactly those agents (owner + date); email-scoped; excludes deleted/system. 11/11 enterprise unit tests.
  • Chat: as the shared user, a rostered agent replied through the portal's endpoint ("Hey there, friend!").
  • Frontend served live (container, :5199): both SFCs transform clean; route guard passes (client_portal entitled); /api proxy 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

dolho and others added 2 commits July 6, 2026 15:31
…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>
@dolho dolho changed the title feat(enterprise): Client Portal "My Agents" roster page (trinity-enterprise#78) feat(enterprise): Client Portal — My Agents roster + chat (trinity-enterprise#78) Jul 6, 2026
…-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 vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/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 both decode_token and get_current_user (mirrors MFA_CHALLENGE_SCOPE); create_access_token mints jti so decode_portal_session's revocation check is live; token carries no sub so 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:

  • ⚠️ The dependencies.py fence 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#78 is 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

@vybe
vybe merged commit c70d2b2 into dev Jul 6, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants