Identity and access
How Hubzoid knows who is asking and decides, in code before any tool runs, who may open an agent and use each of its capabilities.
Hubzoid signs people in with its own accounts and decides what each person may do with grants: one row per person, permission and agent. The access guard checks those grants in code before a tool runs, so the model is never the gate.
When to use it
Read this before you put a tool in restricted/, grant access to a person or to the account a workflow runs as, offer a report in chat, or open a chat surface such as Slack or WhatsApp to restricted tools. For the hands-on version, follow restrict a tool. To manage grants on screen, see agents and access.
A quick example
The finance Hub has a restricted tool in restricted/ledger.py. Grant Priya the ledger permission:
hubzoid grant priya@example.com ledger ./financeGranting ledger also grants Use this agent in finance, so Priya can open the agent and the agent can call ledger_lookup for Priya. When Lee, who has no grant, asks the same question, the tool is hidden from Lee's turn. If a prompt makes the model call it anyway, the guard refuses and the model receives a message it can explain:
[access denied: 'ledger_lookup' requires the 'ledger' permission, which the current user does not have. This attempt was logged.]Both decisions are in Settings → Activity → Audit log → Tool decisions.
How it works
- Caller identity
- Surface check
- Account check
- Grant check
- Decision logged
- Tool runs
What each check does and when it refuses
- Caller identity. Each request binds an identity: the account's email and the surface the request arrived on. Outside a request, for example in
hubzoid test, the identity is anonymous. - Surface check. An anonymous caller is refused. A caller whose surface may not reach restricted tools is refused before any grant is read. A grant is necessary but never sufficient.
- Account check. A blocked account is refused. If the access store cannot be read, the call is refused.
- Grant check. The store answers whether the caller holds the permission in this agent.
- Decision logged. The guard writes one row to
hz_access_decisions. If that row cannot be written, the call is refused even when it was allowed, so every call that ran has a row. - Tool runs. Only now does the tool's own code execute.
Accounts and sign-in
Hubzoid owns the accounts people sign in with, in its operational database. One account works for the web app, Settings, hosted MCP and every agent when several Hubs run together. An account is not access: what someone may use is granted per agent. Authentication covers providers, sessions and the first administrator.
Local mode, signing in with sign-in on, and two-step sign-in
- Local mode. Sign-in is off by default. Every request to the local machine is the local owner,
admin@localhost, an administrator who owns every Hub from the first start. The public port stays on 127.0.0.1. - Sign-in on. With
HUBZOID_AUTH=true(orsign_in: trueinhubzoid.yaml), people sign in with a password, Google, Microsoft or one OpenID Connect provider. Administrators add people with Add user in Settings orhubzoid admin create, which give a one-time sign-in link to share. Nothing is emailed. Self sign-up is off by default and waits for an administrator's approval when turned on. - Two-step sign-in. By default administrators must use a code from an authenticator app. After their first password sign-in they are taken through set-up, then given ten one-time recovery codes. An administrator chooses who must use it (
none,adminsorall) under Organization → Sign-in or withhubzoid auth mfa policy. People who sign in through a company directory (SAML) use that provider's own policy.


Grants
A grant is one row: a subject holds a permission in a domain. Grants live in the hz_grants table and are evaluated with Casbin, using direct grants only. No role or group carries permissions, so "what can this person do in this agent?" has exactly one answer in one table.
The parts of a grant and how names are compared
| Part | Values |
|---|---|
| Subject | A person, or an ordinary account a workflow runs as, keyed by the email they sign in with. An email can be granted before its account exists, with hubzoid grant or the management API. |
| Domain | One agent, named by its Hub folder name in lowercase, such as finance. Or *, the organization, where only manage_access can be granted. |
| Permission | One of the capabilities below. Settings shows each by its label. |
Subjects, domains and permissions are compared in lowercase with spaces trimmed, so Ledger and ledger are the same. A Hub deployed from Git without its .hubzoid/ folder denies everyone until someone is granted access.
Capabilities
Each capability is one permission an agent can grant. Use this agent (use_hub) opens the agent, and each file in restricted/ adds a permission of its own. Nobody holds any capability except through a grant.
Every capability and what it allows
| Permission | Label in Settings | What it allows |
|---|---|---|
use_hub | Use this agent | Open the agent: chat with it, read its knowledge and use its unrestricted tools. |
manage_access | Manage access | Grant and remove access in one agent, or in every agent when held in *. In one agent it also includes chat with that agent, not restricted tools. It also lets a person apply or reject hub changes. |
One per restricted/<module>.py | The file stem, or the label in identity/permissions.yaml | Call every tool defined in that file. restricted/ledger.py defines ledger. |
curator | Save shared knowledge | Propose changes to the agent's knowledge and instructions with remember and propose_hub_change, when in-app editing is on. See memory and history. |
connector_<id> | Connect, or Use for a shared key, with the app's name | Use a connector the agent offers, with your own connection or the company's shared key. |
channel_<id> | Start work from, with the channel's id | Start work for the agent from a channel connection, such as a Slack app or a GitHub repository. |
workflows_view | See workflows and runs | List the agent's workflows and recent runs from chat or an assistant. |
workflows_manage | Run and control workflows | Start a team run, pause or resume a schedule and cancel a run from chat or an assistant, and use Run now in Settings. |
wf_<workflow> | Run, with the report's name | Run one report offered in chat, for yourself and as yourself. |
schedule_reports | Schedule reports for themselves | Save personal schedules of the reports you can run. |
share_domain_links | Share reports with the company domain | Give a report a link that anyone at the company's allowed email domains can read after signing in. |
share_public_links | Share artifacts publicly | Create links to your own artifacts that anyone with the link can open without signing in. |
access_tools | Manage access from chat | Use the access tools in chat or an assistant, in the agents you manage. Every change is confirmed in Settings. |
jev | Call Jev | Use the experimental call_jev tool in chat. |
Workflow permissions on an agent's Access tab adds finer rights per workflow, such as run, schedule, see runs or pause, managed with hubzoid workflow-access. Settings marks sharing, workflow control, access tools and every connector as sensitive. Only organization administrators grant manage_access and access_tools.


Rules for every change
These apply to every write, from Settings, the CLI or the API:
- Granting any permission other than
use_hubin an agent also grantsuse_hubthere. Anyone who can use a tool can open the agent that has it. - Removing
use_hubremoves every permission the subject holds in that agent. - An agent administrator (
manage_accessin one agent) grants or removes only the capabilities they hold there themselves, nevermanage_access, never their own access and never an organization administrator's. - New grants to
*, everyone signed in, are refused. An agent that already had one shows an Everyone signed in row that keeps working until an organization administrator removes it. - The last organization administrator cannot be removed, demoted or deleted.
- A change applies on the next check in every bridge that shares the database, with no restart.
Phone numbers for WhatsApp and Telegram
An organization administrator records the number a person's WhatsApp and Telegram messages come from, under People, the person, Phone, or in Add user. A sender with that number is that person and uses their grants. Numbers are unique across the deployment. A Hub can also name senders whose numbers are not recorded with its own lookup in identity/access.py. A sender found in neither is rejected before the agent runs. identity/access.csv is no longer read, and a Hub that still has one gets a warning.
Surfaces
The surface records the channel a request came through. By default only surfaces where each request carries one verified identity may reach restricted tools: web, app, api, mcp and workflow.
Every surface, who the caller is and whether it reaches restricted tools
| Surface | Where it comes from | Who the caller is | Restricted tools by default |
|---|---|---|---|
web | The Hubzoid web app | The signed-in account, or the local owner with sign-in off | Yes |
app | Chat inside your own app | The person your app vouches for, limited to the agents and tools the app allows | Yes, within the app's limit |
api | Direct callers of the bridge's /v1 endpoint | The person a valid identity assertion names | Yes |
mcp | The hosted MCP server | The account that signed in and approved the connection | Yes |
workflow | Code workflows and markdown tasks | The account the run acts as | Yes |
slack-dm | A Slack direct message or the assistant sidebar | The sender's Slack profile email when SLACK_IDENTITY_MAPPING=true | No |
slack-channel | A mention in a Slack channel or group thread | The person who mentioned the bot, when mapping is on | No, and it must stay off |
whatsapp, telegram | Inbound chat surfaces | The person whose phone number is recorded, else the Hub's lookup | No |
system | No request identity, for example hubzoid test | Anonymous | Always refused |
Change the list with HUBZOID_RESTRICTED_SURFACES. It is a comma-separated, complete list that replaces the default, and it also decides which surfaces may use connectors added in Settings. Restart the Hub after changing it.
HUBZOID_RESTRICTED_SURFACES=web,app,api,mcp,workflow,slack-dmKeep workflow and app in the list
Because the variable replaces the default, leave workflow in it if your
workflows or markdown tasks use restricted tools, and app if your own app's
chat does. Without them, their calls are refused with the reason
surface:workflow or surface:app.
Never add slack-channel
A channel thread combines messages from many people into one prompt, then
answers as the person who mentioned the bot. Anyone in the thread could steer
a tool that only that person may use. A slack-dm conversation carries one
person and is the Slack surface to opt in.
Where the workflow, report and access tools are offered
The workflow, report and access tools follow their own rule: they work in the web app, the API, MCP, WhatsApp and Telegram for people who hold the capability, and are never offered in Slack or inside a scheduled run. See Slack and WhatsApp and Telegram.
Entering an agent
Every chat request checks Use this agent before any tool is involved.
How each door checks the caller
The web app takes the caller from the signed-in session. The bridge's /v1 endpoint takes the caller only from headers covered by a signed identity assertion from another Hubzoid process, never from the request body. A request with no verified identity, or without Use this agent, is refused. If the access store cannot be read, the request fails with 503 rather than passing. Hosted MCP applies the same check on every call, and a blocked account is turned away at every door.
Hidden versus refused
Each restricted tool gets two layers, built from the same tool registry that every runtime uses. Hiding is the clean experience. The refusal at call time is the wall.
How each runtime hides and refuses, and what is logged
| Where the agent runs | Hidden from callers without access | Refused and logged when called |
|---|---|---|
| OpenAI Agents SDK runtime (LiteLLM models) | Yes. The tool's is_enabled check runs the same decision for each run, so the model never sees the tool. | Yes |
Claude Agent SDK runtime (claude-local) | Yes. The tool is left out of the tools that turn offers the model. | Yes |
Local Codex runtime (codex-local) | Yes. The tool is left out of that turn's tools. | Yes |
| Hosted MCP server | Yes. tools/list is filtered with the same decision. | Yes. The refusal returns as an MCP error result. |
The refusal holds when a tool is reached some other way: a prompt that names it, or an MCP client that calls a tool by name. Hiding writes nothing, so a person who never sees a tool leaves no row in the log. Only calls are logged. The gated built-in tools, such as remember and the workflow tools, are hidden and refused the same way.
Two more details keep the wall intact. Sub-agents that the main agent delegates to are built from the same guarded registry. When a restricted tool has the same name as a built-in or tools_local/ tool, the restricted one replaces it, so an unguarded copy can never shadow it.
The decision log
Each decision is one row in hz_access_decisions, written at the moment of the call, on every surface.
Every reason a decision can record
reason | Decision | Meaning |
|---|---|---|
grant | allow | The subject holds the permission |
app | allow | Your own app's limit allows it |
no-grant | deny | No grant matches |
anonymous | deny | No signed-in caller |
surface:<name> | deny | That surface may not reach restricted tools |
blocked | deny | The account is blocked |
store-error | deny | The access store could not be read |
Each row also has the time, agent, caller, surface and tool. Read it under Settings → Activity → Audit log → Tool decisions, or on the server:
hubzoid audit ./finance --denied --user priya@example.com --limit 100Changes to access are a separate record, hz_access_audit, under Access changes on the same screen.
What the access change record holds
It holds the actor, action, subject, agent and permission of every grant, removal, account creation, approval, password reset, role change, deletion and block, every schedule pause and run cancellation, every run started from chat, and sign-in events.
Fail-closed behavior
Every failure denies. No failure falls back to a more permissive path.
Every failure and its result
| Situation | Result |
|---|---|
| No signed-in caller | Restricted tools refused with anonymous. Entering the agent is refused. |
| The access store is unreachable, or the check errors | Refused with store-error. Entering the agent returns 503. |
| The decision row cannot be written | The call is refused, even if it was allowed. |
identity/access.py raises an error | That lookup names nobody. |
| A different account signs in with an email already bound to an earlier account | That email's direct grants are removed and it is blocked until an organization administrator reviews it. |
| A blocked account | Refused everywhere, including agents with an Everyone signed in row. |
The access model in code
The grant store evaluates every request with this Casbin model. A request is allowed when any grant matches, each part exactly or through the * wildcard.
[request_definition]
r = sub, dom, act
[policy_definition]
p = sub, dom, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = (r.sub == p.sub || p.sub == "*") && (r.dom == p.dom || p.dom == "*") && (r.act == p.act || p.act == "*")Older releases ran workflows as service identities, workflow:<name> and workflow:md:<task>. Runs act as ordinary accounts now, so those grants are kept but not used, and Settings shows them as Legacy service identity.
The first administrator
The configured owner, HUBZOID_ADMIN_EMAIL, gets organization administration and Use this agent on every Hub at their first verified sign-in. hubzoid admin create <email> <hub> --owner does the same for a named account, including on a Hub that already ran in local mode. Other people receive nothing automatically, and a later sign-in does not restore access that was removed.
For a deployment nobody can administer, hubzoid access bootstrap makes an organization administrator from the server:
hubzoid access bootstrap --admin you@example.com ./financeAdministration does not include every tool capability. Grant Use this agent separately for chat. hubzoid doctor warns about an agent nobody may use. The whole procedure is in access administration.
Related
Restrict a tool
Gate a tool behind a permission, grant it and read the decision end to end.
Agents and access
Add users, grant capabilities and review changes in Settings.
People and activity
Account states, phone numbers and the two activity records used for audits.
Authentication
Sign-in providers, two-step sign-in and sessions.

