Skip to content

[FEATURE] Session sharingΒ #1933

@onematchfox

Description

@onematchfox

πŸ“‹ Prerequisites

πŸ“ Feature Summary

Support sharing of sessions between authenticated users

❓ Problem Statement / Motivation

kagent sessions are currently private to the user who created them. This creates
two friction points:

  1. Collaboration - there is no way for one user to share a chat session with
    a colleague for review, handoff, or other joint usage.
  2. Agent workflows - agent worklows that run using service account
    credentials have no way to surface the session to a wider audience without
    the recipient having independent access to the session.

This proposal describes a mechanism for sharing sessions across users, with
support for both sharing via API/UI and autonomous sharing via agent tools.

πŸ’‘ Proposed Solution

This is a full design proposal of code I am currently working on. So I will open up a companion PR for this issue. But I figured that if there are any high-level comments/concerns/feedback then these could be addressed here and I will keep the PR in-line with it and we can discuss the nitty-gritty of the implementation details over on the PR itself.

Goals

  • Allow a session owner to generate a shareable link that any authenticated user
    can open.
  • Allow the session owner to choose whether a share link is read-only (view
    only) or read-write (interactive). Read-only is the default.
  • Allow agents to generate and revoke share links as part of their own
    workflows.
  • Keep the schema and middleware extensible enough to support richer access
    models (per-user ACLs, expiry) without a redesign.
  • Within a non-read-only shared session, record which user initiated each action
    in a shared session so attribution is available.
  • Ensure that shared sessions that a user has accessed are discoverable and
    surfaced in their sidebar alongside owned sessions, so users do not need to
    retain the original link to return to a shared session.

Non-goals (this iteration)

  • ACLs - sharing with specific named user(s)/group(s) only
  • Share expiration.
  • Display-name resolution in the UI for attribution for shared-session
    interatcion (user IDs are stored; human-readable names are a future step).
  • A forking model - where a visitor can branch off a shared session and make
    changes that only they see.

Collaborative model

Shared sessions support two modes, selectable per share link:

  • Read-only (default): visitors see the conversation but cannot send
    messages or respond to tool confirmations. This is appropriate for review,
    broadcast, and "share the output" use cases.
  • Read-write (interactive): visitors see the session and can interact with
    it as if they were the owner - they send messages, approve/reject tool calls
    and answer agent questions. All parties will see the results of this
    interaction. This supports handoff and joint troubleshooting use cases.

Architecture Overview

sequenceDiagram
    participant Visitor as Visitor (browser)
    participant UA as UI / Agent
    participant Next as Next.js chat route
    participant MW as shareTokenMiddleware
    participant Server as HTTP Server
    participant Handlers as Session handlers / A2A PassthroughMgr

    note over UA,Server: Creation path
    UA->>Server: POST /api/sessions/{id}/shares
    Server-->>UA: { token }

    note over Visitor,Handlers: Visitor path
    Visitor->>Next: GET /agents/{ns}/{name}/chat/{id}?share=<token>
    Note over Next: extracts token from ?share query param
    Next->>MW: API/A2A calls with X-Share-Token header
    MW->>MW: validate token, record access, inject ShareContext<br/>(carries owner's user ID)
    MW->>Handlers: request with ShareContext
    Handlers-->>Visitor: session data / streamed response
Loading

Database Schema

Two new tables:

CREATE TABLE session_share (
    id         BIGSERIAL   PRIMARY KEY,
    token      TEXT        UNIQUE NOT NULL,
    session_id TEXT        NOT NULL,
    user_id    TEXT        NOT NULL,   -- session owner's user ID
    read_only  BOOLEAN     NOT NULL DEFAULT TRUE,
    created_at TIMESTAMP   NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_session_share_session_id ON session_share (session_id);

CREATE TABLE session_share_access (
    user_id     TEXT    NOT NULL,
    share_id    BIGINT  NOT NULL REFERENCES session_share(id) ON DELETE CASCADE,
    accessed_at TIMESTAMP NOT NULL DEFAULT NOW(),
    PRIMARY KEY (user_id, share_id)
);

token is a 48-character lowercase hex string (24 cryptographically random
bytes). At this length there are approximately 2¹⁹² possible values -
brute-force enumeration is not a viable attack.

id is a database-generated BIGSERIAL. Application code never generates or
stores the ID β€” it is only used internally as a foreign key target for
session_share_access, keeping the capability token out of other tables.

user_id is the session owner's identity and is used by the middleware to
perform DB lookups on the owner's behalf.

read_only defaults to TRUE at both the database and API level. The POST /api/sessions/{id}/shares handler treats an absent read_only field as true.
Pass "read_only": false explicitly to create a read-write share link.

session_share_access records which authenticated users have previously opened
a share link. A row is upserted (with accessed_at refreshed) each time the
middleware successfully validates a token. ON DELETE CASCADE ensures access
records are automatically removed when the share is revoked β€” no application-level
cleanup is needed.


HTTP API

Three endpoints are added under the existing /api prefix:

Method Path Auth Description
POST /api/sessions/{session_id}/shares Owner only Create a new share token for a session
GET /api/sessions/{session_id}/shares Owner only List all active tokens for a session
DELETE /api/sessions/{session_id}/shares/{token} Owner only Revoke a specific token

"Owner only" means the server verifies that the authenticated caller's user ID
matches the session's owning user before proceeding - unauthenticated callers
and callers who do not own the session both receive 404.

POST /api/sessions/{id}/shares accepts an optional JSON body:

{ "read_only": false }

When the body is absent or read_only is omitted, the server defaults to true
(read-only).

The existing GET /api/sessions/agent/{namespace}/{name} endpoint is extended
to return previously-accessed shared sessions alongside owned ones. The response
type gains share_token and share_read_only fields (null for owned sessions,
populated for accessed shared sessions).

The existing GET /api/sessions/{id} endpoint gains a top-level read_only
field in its response. This field is always present: true when the request was
authenticated via a read-only share token, false otherwise
(owner-authenticated or read-write share). The frontend uses this authoritative
value to decide whether to render the input form - it does not infer the state
from URL parameters.

Authentication Model

X-Share-Token header

Clients pass the share token in a request header rather than a query parameter
or path segment. This:

  • Is consistent with the existing Authorization header pattern.
  • Allows the frontend to add the header to both REST and A2A streaming calls
    without URL changes.

ShareContext

When the middleware validates a token, it stores a ShareContext in the request
context:

type ShareContext struct {
    Token     string // raw share token
    SessionID string // session this token grants access to
    UserID    string // owner's user ID - used for DB lookups
    ReadOnly  bool   // when true, only read operations are permitted
}

Session read handlers check for a ShareContext and, when present, use
UserID for database queries instead of the caller's own user ID. This
allows a visitor to read session data that belongs to another user while the
authorisation remains explicit and server-enforced - there is no path where a
visitor can access sessions they were not specifically granted access to via a
valid token.

When ReadOnly is true, the middleware rejects any POST, PUT, PATCH, or
DELETE request to session or A2A paths with 403 Forbidden before it reaches
any handler. Requests to unrelated endpoints (e.g. submitting feedback, creating
new sessions) are not affected. This enforcement is server-side and
unconditional β€” there is no client-side value the caller can send to bypass it.

The GET /api/sessions/{id} response always includes a read_only field. The
frontend uses this authoritative server value (not a URL parameter) to decide
whether to render the input form.

Action attribution

When a visitor sends a message or submits a tool response via a shared session,
the server injects initiated_by: <visitor_user_id> into message.metadata.
This is handled in the A2A PassthroughManager:

func injectInitiatedBy(ctx context.Context, msg *protocol.Message) {
    if _, ok := ShareContextFrom(ctx); !ok {
        return  // not a shared session - nothing to annotate
    }
    session, ok := AuthSessionFrom(ctx)
    if !ok || session.Principal().User.ID == "" {
        return
    }
    msg.Metadata["initiated_by"] = session.Principal().User.ID
}

The visitor's identity is read from the auth session already present in the
context - ShareContext deliberately does not duplicate it. This metadata is
stored alongside the message and can be surfaced in the UI or queried
programmatically.


Frontend

Share button

The UI adds a share button to the chat toolbar. Clicking it opens a modal with
two toggles:

  1. Private / Shared toggle (globe / lock icon): enables or disables sharing.
    Defaults to Private. Toggling to Shared calls POST /api/sessions/{id}/shares (using the current read-only setting) and renders
    the resulting URL. Toggling back to Private calls GET /api/sessions/{id}/shares to retrieve all active tokens and then DELETE /api/sessions/{id}/shares/{token} for each one, revoking all access
    immediately regardless of how many tokens exist.

  2. Read-only toggle: controls whether visitors can send messages. Defaults
    to read-only. This toggle is only active before a share is created - once
    a share exists it is disabled. To change the read-only setting on an existing
    share, the user must disable sharing and re-enable it.

On modal open, the UI calls GET /api/sessions/{id}/shares to check for an
existing token, so both toggles reflect the true current state. If a token
already exists (e.g. one created by an agent), the UI surfaces it directly
rather than creating a duplicate.

Share URL format

Share links use the existing chat URL with the token appended as a query
parameter:

/agents/{namespace}/{name}/chat/{sessionId}?share={token}

This keeps all session links structurally identical - the sidebar, the browser
history, and shared links all follow the same URL pattern. The Next.js chat
route reads the ?share query parameter and passes the token as X-Share-Token
on all downstream API and A2A calls.

Sessions sidebar

The sessions sidebar shows sessions the current user has access to for an agent:

  • Owned sessions β€” always shown.
  • Previously accessed shared sessions β€” shown if the user has previously
    opened a valid share link for that session. Access is recorded automatically
    by the shareTokenMiddleware on the first (and each subsequent) visit.

Shared sessions are visually distinguished with a small indicator icon. Clicking
one opens the chat with the share token in the URL so the ?share= parameter is
automatically present on all API calls.

Sessions with active share tokens that the current user has not yet visited
are not shown in the sidebar. Users must have the link to access them for the
first time, at which point the session is added to their sidebar automatically.

If a share token is revoked after a user has accessed it, the ON DELETE CASCADE
on session_share_access removes the access record, and the session disappears
from that user's sidebar on the next page load.

The sidebar data comes from a single GET /api/sessions/agent/{namespace}/{name} call. The server resolves previously
accessed shared sessions by joining session β†’ session_share β†’
session_share_access filtered by the caller's user_id. Each entry includes
an optional share_token field: null for owned sessions, populated with the
active token for sessions accessed via a share link.


Agent SDK Tools

Three tools are implemented in both the Python ADK (kagent-adk) and the Go
ADK:

Tool API call Description
create_share_link POST /api/sessions/{id}/shares Creates a token; returns the full share URL. Accepts optional read_only parameter (defaults to true).
list_share_links GET /api/sessions/{id}/shares Lists active tokens and creation times
delete_share_link DELETE /api/sessions/{id}/shares/<token> Revokes a specific token by value

The tools obtain the session ID from the ADK runtime context (no parameter
required) and authenticate outbound requests using the Kubernetes service
account token and the caller's forwarded user ID. Because create and delete are
owner-only on the server, these tools can only act on the session they are
running within.

The create_share_link tool creates read-only links by default. Pass
read_only: false to create an interactive share. The tool returns a full
absolute URL when the KAGENT_UI_URL environment variable is configured on the
agent pod (see Deployment Configuration); otherwise
it returns a relative path
(/agents/{namespace}/{name}/chat/{sessionId}?share={token}). This is
controlled by the controller.externalUrl Helm value, which the controller
stamps into every agent pod it spawns.

Example usage pattern:

An agent asked to "share the results of this investigation with the team" would:

  1. Call create_share_link to generate a token.
  2. Return the resulting URL in its response.

An agent with cleanup responsibilities could call list_share_links followed by
delete_share_link to revoke access once the need has passed.

Opt-in via CRD field

The share tools are not added to any agent by default. They are opt-in via a
dedicated field on DeclarativeAgentSpec:

apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
  name: my-agent
  namespace: kagent
spec:
  type: Declarative
  declarative:
    description: "An agent that can share its results"
    systemMessage: "You are a helpful assistant..."
    modelConfig: gpt-4o
    shareTools: true   # enables create_share_link, list_share_links, delete_share_link

When shareTools: true is set, the controller propagates this flag through to
the runtime's AgentConfig, which injects the three tools at agent startup.
Omitting the field (or setting it to false) leaves the agent's tool list
unchanged.

This design gives operators full control: an agent does not automatically gain
the ability to publish session URLs unless explicitly granted that capability.

Deployment Configuration

For share tools to return full clickable URLs, set the controller.externalUrl
Helm value to the public-facing base URL of the kagent deployment:

controller:
  externalUrl: "https://kagent.example.com"

The controller reads this value as KAGENT_UI_URL and injects it into every
agent pod. If left unset, share tools return a relative path which the model
cannot turn into a clickable link, so this value should always be set in
production.


Token Multiplicity

The API intentionally supports multiple active tokens per session. A primary
motivating future use case is per-audience access: one read-write token for
collaborators and one read-only token for observers, each revocable
independently. The current schema already accommodates this.

Today's UI presents a simplified view: the share modal displays tokens one at a
time. When multiple tokens exist (for example, one created via the UI and one
created by an agent), the modal shows the first token returned by GET /api/sessions/{id}/shares. This means the modal may not surface all active
tokens if tokens have been created directly via the API or by agent tools.

Disabling sharing is always a complete revocation. When the user toggles the
Private/Shared control to Private, the UI first calls GET /api/sessions/{id}/shares to retrieve all active tokens and then deletes each
one individually - not just the one currently displayed. This ensures
agent-created or API-created tokens are also revoked and prevents the UI from
leaving orphaned tokens that would allow continued access. Because
session_share_access has ON DELETE CASCADE, revoking a share automatically
removes all access records for that token.

Future iterations of the UI may surface all active tokens with their individual
settings and allow managing each independently.


Security Considerations

Tokens are unguessable. 48-hex-character tokens (24 random bytes from
crypto/rand) provide approximately 2¹⁹² possible values. Enumeration attacks
are computationally infeasible.

API protection: Read-only vs read-write mode is enforced server-side: the
shareTokenMiddleware rejects any non-GET/HEAD request to session or A2A
paths carrying a read-only token with 403 Forbidden, regardless of what the
client UI renders.

Owner-only mutation. Create, list, and delete operations verify session
ownership server-side on every call. A visitor who has a share token cannot
create additional tokens, list all tokens, or revoke tokens for that session.

Scoped access. A share token grants access to exactly one session. It cannot
be used to enumerate other sessions belonging to the owner, modify session
metadata, or access any other resource.

Revocation is immediate. Deleting a token removes the session_share row.
Any subsequent request carrying that token receives 403. The ON DELETE CASCADE
on session_share_access simultaneously removes all access records. There is no
cache or grace period.

Share lifetime follows session lifetime. Sessions and agents use soft-delete
throughout the codebase (deleted_at timestamp, no cascades). Share rows are
deliberately not cleaned up when a session or agent is soft-deleted, consistent
with this pattern and to support potential undeletion. If a session is
permanently purged (e.g. by a future hard-delete or cleanup job), the
corresponding share rows should be removed at that point alongside all other
child data (events, tasks, push notifications).

Sidebar privacy. Shared sessions only appear in a user's sidebar after they
have previously accessed the share link. Unauthenticated or uninvited users
cannot discover shared sessions by browsing the sidebar.

Visitor identity is preserved. The initiated_by metadata field ensures
that actions taken by visitors are attributable to a real authenticated user ID,
not laundered through the session owner's identity.

No backend URL construction. The backend API never constructs or returns the
full share URL - it returns only the token. The frontend constructs
<origin>/agents/{ns}/{name}/chat/{id}?share={token} client-side, and agent
tools construct the URL from the KAGENT_UI_URL environment variable configured
at deployment time. This keeps the token assembly out of server-side code and
avoids hard-coding a public hostname in backend configuration.

Referrer header leakage. The share token appears in the browser URL as a
query parameter. If a shared chat page loads any external resources (scripts,
images, fonts), the token will be present in the Referer header sent to those
origins. Shared chat pages must include <meta name="referrer" content="no-referrer"> to suppress this.


Extensibility

The schema and middleware are deliberately minimal. Each of the following
extensions requires only additive changes:

Token expiry Add an expires_at TIMESTAMP column (nullable). The middleware
lookup adds AND (expires_at IS NULL OR expires_at > NOW()). No existing rows
or callers are affected. The create endpoint can accept an optional ttl
parameter.

ACLs: Adding an allowed_group_ids/allowed_user_ids column(s), or a
separate join table(s), could scope tokens to a user/group membership.

Session forking A visitor could request a fork of a shared session -
creating a new session seeded with the existing message history. The fork is
owned by the visitor and is fully independent of the original. This would allow
non-collaborative exploration without affecting the shared session and could
even be enabled for read-only sessions and/or sessions in general.

All of these extensions are purely additive at the database and middleware
level. Existing tokens issued under the current schema remain valid and their
access model is unchanged.

πŸ”„ Alternatives Considered

No response

🎯 Affected Service(s)

Multiple services / System-wide

πŸ“š Additional Context

No response

πŸ™‹ Are you willing to contribute?

  • I am willing to submit a PR for this feature

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for Feature.

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions