Permissions & access model
How Martha decides who you are (authentication) and what you may use (authorization). The CLI guide lists the grant commands; this page is the model they operate on. If you've ever hit "why can't my agent call this tool?", the answer is almost always here.
One sentence: Keycloak establishes the principal (a tenant + a Client or an Agent); Martha's grant tables are ACLs that say which capabilities that principal may invoke. There is no per-user tool permission.
Identities
| Identity | What it is | Example |
|---|---|---|
| Tenant | The isolation boundary. Every definition, grant, session, and message is scoped to one tenant. A string id. | acme |
| Client | An API-key / chat consumer within a tenant. An integer id. Tools are granted to clients. | my-bot |
| Agent | An AgentDefinition (system prompt + LLM config) with its own identity. A UUID. Tools can also be granted to agents. | support-bot |
| Human user | A Keycloak user. Has roles, not tool grants. | you |
A tenant has many clients and agents. Client.id (int) is not the tenant — it's a resource inside one.
Authentication (Keycloak, realm frank)
Three principals authenticate three ways:
- Human → client
frank-low-code, browser PKCE or password grant.martha auth login(or--username/--password). Tokens are short-lived (~5 min). - Service account (CI) →
client_credentials.MARTHA_CLIENT_ID/MARTHA_CLIENT_SECRET+martha auth login --service-account, or export a pre-issuedMARTHA_TOKEN. Auto-refreshes. - Agent → a per-agent confidential Keycloak client
martha-agent-{uuid}(created when the agent is provisioned for auth) viaclient_credentials, or anmartha_ak_…API key. Either resolves tosub="agent:{id}",is_service_account=true.
A token carries a required tenant_id claim (missing → 403) and realm roles. Roles gate privilege:
admin— tenant admin.martha-super-admin— platform admin (cross-tenant).client:{client_key}— explicit access to one client.
Humans vs service accounts matters: privileged, human-only actions (e.g. resolving an approval) require a human token; agents and service accounts are rejected at the auth layer.
Authorization: catalog ≠ grant
Two different things, often confused:
- Catalog —
function_definitions/workflow_definitions. A capability exists in your tenant (platform functions are auto-seeded). This is not permission. - Grant — a row in a junction table. A specific Client or Agent may invoke a capability.
An agent does not see a tool just because it's in the catalog. It needs an explicit grant. "Registered" ≠ "usable."
Grants attach to a Client or an Agent — never to an individual human user. There is no UserFunctionAccess. A human's roles only decide which client they may drive; the tools then come from that client's (and, in chat, the agent's) grants.
Grant the access
# grant a capability to a client (type ∈ function | workflow | agent | mcp)
martha clients grant my-bot function search_documents
martha clients grant my-bot agent support-bot # which agents this client may invoke
# collection-scoped grant (the function only sees that collection + descendants)
martha clients grant my-bot function list_docs --collection reports
# grant to an AGENT (chat / agent-loop surface)
martha agents add-function search_documents # + --collection <slug-or-id>
martha clients revoke my-bot function search_documents
martha clients access my-bot # show all active grantsGrants are an allowlist (no per-row deny) and fail closed: no matching grant → the call is denied (a clean 403-style result), never a crash. Collection-scoped grants resolve by most-specific ancestor across the union of client and agent grants.
The two-surface rule (read this before debugging a 403)
The same function is authorized differently depending on how it's invoked:
| Invocation | Runs as | Grant it needs | CLI |
|---|---|---|---|
| Chat / agent-loop | the agent (+ optional linked client) | AgentFunctionAccess | martha agents add-function |
| Workflow function-node | the triggering caller's Client (run-as) | ClientFunctionAccess | martha clients grant … function … |
| REST / direct call | the Client (API key / token) | ClientFunctionAccess | martha clients grant … |
The classic trap: you
agents add-function X, the agent calls X fine in chat, but a workflow that uses X returns a 403. Workflow nodes run as the client, not the agent — grant the client (clients grant). An agent grant is inert for workflow nodes.
This separation is deliberate: revoking an agent's tool grant never changes how workflows execute, and vice versa.
Tenant isolation
Everything is filtered by tenant_id. Grants are implicitly tenant-scoped (a grant references a definition that belongs to a tenant). A principal in tenant A can never resolve, see, or invoke anything in tenant B. Platform functions (source='platform') are the one shared catalog — seeded once and visible to every tenant — but still require a grant to invoke.
See also
- Surfaces & capability keys — how a chat client/embed declares what it can render and answer.
- Capability approvals — gating risky capabilities behind human approval.