DOCS
Policies
Sveda does not store your users. Your app keeps the role. The embed token carries a policy name. The runtime enforces it.
Three layers
Role lives in the host app. Policy is a named entry under policies in sveda.yaml. Capability is what the token is allowed to advertise and call on each turn.
Mint and enforce
POST /sveda/embed/token accepts policy and optional grants. Unknown policy names return 422. Tokens without a policy stay unrestricted. GET /sveda/embed/config returns capabilities for the UI; the token is what the runtime enforces.
Where each piece lives
- Role — your app (Spatie, Django groups, an AD group). Sveda never reads that name.
-
Policy — a named entry under
policiesinsveda.yaml. The name is what the host sends aspolicywhen it mints a token. - Capability — what that token may advertise and call. Enforced on every turn.
The host maps the signed-in user to a policy name when it mints the token. A browser cannot change that by substituting an email: the session user comes from the host, and the token is HMAC-signed.
Policy document
Globs are only *,
prefix*, and
*suffix.
max_mode is
read <
write <
delete.
When a policy is set, omitted web and
code are denied. An empty
allow list means no tools of that class.
Tokens with no policy keep the previous unrestricted behavior.
policies:
reader:
web: false
code: false
mcp:
allow: ["tickets.*"]
domains: ["tickets"]
max_mode: read
client:
allow: []
agent:
web: true
code: false
mcp:
allow: ["*"]
max_mode: delete
client:
allow: ["*"]policies:
reader:
web: false
code: false
mcp:
allow: ["tickets.*"]
domains: ["tickets"]
max_mode: read
client:
allow: []
agent:
web: true
code: false
mcp:
allow: ["*"]
max_mode: delete
client:
allow: ["*"]
Tools with MCP mode
delete
(for example
delete_post
) need
max_mode: "delete"
and the tool name in
mcp.allow
or the model never sees them. Chat confirmation is separate — see
tool confirmation.
Optional grants on mint can only tighten the named policy. An unknown policy name returns 422. Which rows a tool may touch stays in the host tool handler.
Mint
POST /sveda/embed/token
x-sveda-host-key: $SVEDA_EMBED_HOST_API_KEY
{
"visitor_id": "user-42",
"policy": "reader"
}POST /sveda/embed/token
x-sveda-host-key: $SVEDA_EMBED_HOST_API_KEY
{
"visitor_id": "user-42",
"policy": "reader"
} GET /sveda/embed/config
returns capabilities so the widget can hide controls. The runtime still filters tools from the token.
Host mapping
host.policyUsing((user) => (user.role === 'agent' ? 'agent' : 'reader'))
host.resolveToolsUsing((user) =>
user.role === 'agent' ? [searchPosts, createPost, updatePost] : [searchPosts],
)host.policyUsing((user) => (user.role === 'agent' ? 'agent' : 'reader')) host.resolveToolsUsing((user) => user.role === 'agent' ? [searchPosts, createPost, updatePost] : [searchPosts], )
SvedaClient::host()->policyUsing(
fn ($user) => $user->hasRole('agent') ? 'agent' : 'reader',
);SvedaClient::host()->policyUsing(
fn ($user) => $user->hasRole('agent') ? 'agent' : 'reader',
);
End-user web and mobile clients receive the short-lived embed token. They must not hold
SVEDA_EMBED_HOST_API_KEY.