You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have searched the existing issues to avoid creating a duplicate
By submitting this issue, you agree to follow our Code of Conduct
π 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:
Collaboration - there is no way for one user to share a chat session with
a colleague for review, handoff, or other joint usage.
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:
CREATETABLEsession_share (
id BIGSERIALPRIMARY KEY,
token TEXT UNIQUE NOT NULL,
session_id TEXTNOT NULL,
user_id TEXTNOT NULL, -- session owner's user ID
read_only BOOLEANNOT NULL DEFAULT TRUE,
created_at TIMESTAMPNOT NULL DEFAULT NOW()
);
CREATEINDEXidx_session_share_session_idON session_share (session_id);
CREATETABLEsession_share_access (
user_id TEXTNOT NULL,
share_id BIGINTNOT NULLREFERENCES session_share(id) ON DELETE CASCADE,
accessed_at TIMESTAMPNOT 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:
typeShareContextstruct {
Tokenstring// raw share tokenSessionIDstring// session this token grants access toUserIDstring// owner's user ID - used for DB lookupsReadOnlybool// 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:
funcinjectInitiatedBy(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:
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.
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:
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:
Call create_share_link to generate a token.
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/v1alpha2kind: Agentmetadata:
name: my-agentnamespace: kagentspec:
type: Declarativedeclarative:
description: "An agent that can share its results"systemMessage: "You are a helpful assistant..."modelConfig: gpt-4oshareTools: 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:
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.
π 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:
a colleague for review, handoff, or other joint usage.
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
can open.
only) or read-write (interactive). Read-only is the default.
workflows.
models (per-user ACLs, expiry) without a redesign.
in a shared session so attribution is available.
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)
interatcion (user IDs are stored; human-readable names are a future step).
changes that only they see.
Collaborative model
Shared sessions support two modes, selectable per share link:
messages or respond to tool confirmations. This is appropriate for review,
broadcast, and "share the output" use cases.
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 responseDatabase Schema
Two new tables:
tokenis a 48-character lowercase hex string (24 cryptographically randombytes). At this length there are approximately 2ΒΉβΉΒ² possible values -
brute-force enumeration is not a viable attack.
idis a database-generatedBIGSERIAL. Application code never generates orstores 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_idis the session owner's identity and is used by the middleware toperform DB lookups on the owner's behalf.
read_onlydefaults toTRUEat both the database and API level. ThePOST /api/sessions/{id}/shareshandler treats an absentread_onlyfield astrue.Pass
"read_only": falseexplicitly to create a read-write share link.session_share_accessrecords which authenticated users have previously openeda share link. A row is upserted (with
accessed_atrefreshed) each time themiddleware successfully validates a token.
ON DELETE CASCADEensures accessrecords are automatically removed when the share is revoked β no application-level
cleanup is needed.
HTTP API
Three endpoints are added under the existing
/apiprefix:POST/api/sessions/{session_id}/sharesGET/api/sessions/{session_id}/sharesDELETE/api/sessions/{session_id}/shares/{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}/sharesaccepts an optional JSON body:{ "read_only": false }When the body is absent or
read_onlyis omitted, the server defaults totrue(read-only).
The existing
GET /api/sessions/agent/{namespace}/{name}endpoint is extendedto return previously-accessed shared sessions alongside owned ones. The response
type gains
share_tokenandshare_read_onlyfields (null for owned sessions,populated for accessed shared sessions).
The existing
GET /api/sessions/{id}endpoint gains a top-levelread_onlyfield in its response. This field is always present:
truewhen the request wasauthenticated via a read-only share token,
falseotherwise(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-TokenheaderClients pass the share token in a request header rather than a query parameter
or path segment. This:
Authorizationheader pattern.without URL changes.
ShareContextWhen the middleware validates a token, it stores a
ShareContextin the requestcontext:
Session read handlers check for a
ShareContextand, when present, useUserIDfor database queries instead of the caller's own user ID. Thisallows 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
ReadOnlyis true, the middleware rejects anyPOST,PUT,PATCH, orDELETErequest to session or A2A paths with403 Forbiddenbefore it reachesany 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 aread_onlyfield. Thefrontend 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>intomessage.metadata.This is handled in the A2A
PassthroughManager:The visitor's identity is read from the auth session already present in the
context -
ShareContextdeliberately does not duplicate it. This metadata isstored 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:
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 rendersthe resulting URL. Toggling back to Private calls
GET /api/sessions/{id}/sharesto retrieve all active tokens and thenDELETE /api/sessions/{id}/shares/{token}for each one, revoking all accessimmediately regardless of how many tokens exist.
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}/sharesto check for anexisting 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:
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
?sharequery parameter and passes the token asX-Share-Tokenon all downstream API and A2A calls.
Sessions sidebar
The sessions sidebar shows sessions the current user has access to for an agent:
opened a valid share link for that session. Access is recorded automatically
by the
shareTokenMiddlewareon 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 isautomatically 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 CASCADEon
session_share_accessremoves the access record, and the session disappearsfrom 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 previouslyaccessed shared sessions by joining
sessionβsession_shareβsession_share_accessfiltered by the caller'suser_id. Each entry includesan optional
share_tokenfield: null for owned sessions, populated with theactive token for sessions accessed via a share link.
Agent SDK Tools
Three tools are implemented in both the Python ADK (
kagent-adk) and the GoADK:
create_share_linkPOST /api/sessions/{id}/sharesread_onlyparameter (defaults totrue).list_share_linksGET /api/sessions/{id}/sharesdelete_share_linkDELETE /api/sessions/{id}/shares/<token>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_linktool creates read-only links by default. Passread_only: falseto create an interactive share. The tool returns a fullabsolute URL when the
KAGENT_UI_URLenvironment variable is configured on theagent pod (see Deployment Configuration); otherwise
it returns a relative path
(
/agents/{namespace}/{name}/chat/{sessionId}?share={token}). This iscontrolled by the
controller.externalUrlHelm value, which the controllerstamps into every agent pod it spawns.
Example usage pattern:
An agent asked to "share the results of this investigation with the team" would:
create_share_linkto generate a token.An agent with cleanup responsibilities could call
list_share_linksfollowed bydelete_share_linkto 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:When
shareTools: trueis set, the controller propagates this flag through tothe runtime's
AgentConfig, which injects the three tools at agent startup.Omitting the field (or setting it to
false) leaves the agent's tool listunchanged.
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.externalUrlHelm value to the public-facing base URL of the kagent deployment:
The controller reads this value as
KAGENT_UI_URLand injects it into everyagent 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 activetokens 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}/sharesto retrieve all active tokens and then deletes eachone 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_accesshasON DELETE CASCADE, revoking a share automaticallyremoves 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 attacksare computationally infeasible.
API protection: Read-only vs read-write mode is enforced server-side: the
shareTokenMiddlewarerejects any non-GET/HEADrequest to session or A2Apaths carrying a read-only token with
403 Forbidden, regardless of what theclient 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_sharerow.Any subsequent request carrying that token receives 403. The
ON DELETE CASCADEon
session_share_accesssimultaneously removes all access records. There is nocache or grace period.
Share lifetime follows session lifetime. Sessions and agents use soft-delete
throughout the codebase (
deleted_attimestamp, no cascades). Share rows aredeliberately 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_bymetadata field ensuresthat 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 agenttools construct the URL from the
KAGENT_UI_URLenvironment variable configuredat 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
Refererheader sent to thoseorigins. 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 TIMESTAMPcolumn (nullable). The middlewarelookup adds
AND (expires_at IS NULL OR expires_at > NOW()). No existing rowsor callers are affected. The create endpoint can accept an optional
ttlparameter.
ACLs: Adding an
allowed_group_ids/allowed_user_idscolumn(s), or aseparate 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?