Concepts

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:

Terminal
hubzoid grant priya@example.com ledger ./finance

Granting 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:

Output
[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

  1. Caller identity
  2. Surface check
  3. Account check
  4. Grant check
  5. Decision logged
  6. Tool runs
Every call to a restricted tool takes this path. A failure at any step refuses the call.
What each check does and when it refuses
  1. 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.
  2. 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.
  3. Account check. A blocked account is refused. If the access store cannot be read, the call is refused.
  4. Grant check. The store answers whether the caller holds the permission in this agent.
  5. 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.
  6. 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 (or sign_in: true in hubzoid.yaml), people sign in with a password, Google, Microsoft or one OpenID Connect provider. Administrators add people with Add user in Settings or hubzoid 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, admins or all) under Organization → Sign-in or with hubzoid auth mfa policy. People who sign in through a company directory (SAML) use that provider's own policy.
Settings, People: four people with their account status, role, agents, a Two-step column showing On for the administrator Sam Rivera and Off for the others, and when each last signed in.Settings, People: four people with their account status, role, agents, a Two-step column showing On for the administrator Sam Rivera and Off for the others, and when each last signed in.
The Two-step column shows who has set up two-step sign-in.

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
PartValues
SubjectA 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.
DomainOne agent, named by its Hub folder name in lowercase, such as finance. Or *, the organization, where only manage_access can be granted.
PermissionOne 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
PermissionLabel in SettingsWhat it allows
use_hubUse this agentOpen the agent: chat with it, read its knowledge and use its unrestricted tools.
manage_accessManage accessGrant 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>.pyThe file stem, or the label in identity/permissions.yamlCall every tool defined in that file. restricted/ledger.py defines ledger.
curatorSave shared knowledgePropose 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 nameUse a connector the agent offers, with your own connection or the company's shared key.
channel_<id>Start work from, with the channel's idStart work for the agent from a channel connection, such as a Slack app or a GitHub repository.
workflows_viewSee workflows and runsList the agent's workflows and recent runs from chat or an assistant.
workflows_manageRun and control workflowsStart 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 nameRun one report offered in chat, for yourself and as yourself.
schedule_reportsSchedule reports for themselvesSave personal schedules of the reports you can run.
share_domain_linksShare reports with the company domainGive a report a link that anyone at the company's allowed email domains can read after signing in.
share_public_linksShare artifacts publiclyCreate links to your own artifacts that anyone with the link can open without signing in.
access_toolsManage access from chatUse the access tools in chat or an assistant, in the agents you manage. Every change is confirmed in Settings.
jevCall JevUse 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.

Settings, the My Hub agent on its Access tab, People view: Lee Okafor and Priya Raman with Use this agent, Sam Rivera with Use this agent and Manage access inherited, each with an Edit access button.Settings, the My Hub agent on its Access tab, People view: Lee Okafor and Priya Raman with Use this agent, Sam Rivera with Use this agent and Manage access inherited, each with an Edit access button.
Grant and remove capabilities for each person on the agent's Access tab.

Rules for every change

These apply to every write, from Settings, the CLI or the API:

  • Granting any permission other than use_hub in an agent also grants use_hub there. Anyone who can use a tool can open the agent that has it.
  • Removing use_hub removes every permission the subject holds in that agent.
  • An agent administrator (manage_access in one agent) grants or removes only the capabilities they hold there themselves, never manage_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
SurfaceWhere it comes fromWho the caller isRestricted tools by default
webThe Hubzoid web appThe signed-in account, or the local owner with sign-in offYes
appChat inside your own appThe person your app vouches for, limited to the agents and tools the app allowsYes, within the app's limit
apiDirect callers of the bridge's /v1 endpointThe person a valid identity assertion namesYes
mcpThe hosted MCP serverThe account that signed in and approved the connectionYes
workflowCode workflows and markdown tasksThe account the run acts asYes
slack-dmA Slack direct message or the assistant sidebarThe sender's Slack profile email when SLACK_IDENTITY_MAPPING=trueNo
slack-channelA mention in a Slack channel or group threadThe person who mentioned the bot, when mapping is onNo, and it must stay off
whatsapp, telegramInbound chat surfacesThe person whose phone number is recorded, else the Hub's lookupNo
systemNo request identity, for example hubzoid testAnonymousAlways 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.

finance/.env
HUBZOID_RESTRICTED_SURFACES=web,app,api,mcp,workflow,slack-dm

Keep 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 runsHidden from callers without accessRefused 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 serverYes. 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
reasonDecisionMeaning
grantallowThe subject holds the permission
appallowYour own app's limit allows it
no-grantdenyNo grant matches
anonymousdenyNo signed-in caller
surface:<name>denyThat surface may not reach restricted tools
blockeddenyThe account is blocked
store-errordenyThe 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:

Terminal
hubzoid audit ./finance --denied --user priya@example.com --limit 100

Changes 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
SituationResult
No signed-in callerRestricted tools refused with anonymous. Entering the agent is refused.
The access store is unreachable, or the check errorsRefused with store-error. Entering the agent returns 503.
The decision row cannot be writtenThe call is refused, even if it was allowed.
identity/access.py raises an errorThat lookup names nobody.
A different account signs in with an email already bound to an earlier accountThat email's direct grants are removed and it is blocked until an organization administrator reviews it.
A blocked accountRefused 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.

hubzoid/access/store.py (Casbin model)
[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:

Terminal
hubzoid access bootstrap --admin you@example.com ./finance

Administration 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.

Choose which cookies Hubzoid can use. You can change this at any time from Cookie settings in the footer. Read the Cookie Policy for details.