Admin

Tenants

Separate teams or customers in one cluster.

For: cluster and tenant administrators.

Use tenants to separate teams or customers. Assign roles and content access according to the work each person must perform.

A tenant is an isolated team or customer in a cluster. Each tenant has its own:

  • Nodes. A node belongs to one tenant, chosen by its join token or by the operator who accepts it.
  • Plugins. Installing a plugin installs it on that tenant's nodes only. Agent names are unique per tenant.
  • Secrets. A secret sealed for one tenant cannot be opened for another.
  • Temporal namespace, with the same name as the tenant, on one of the cluster's Temporal backends. Jobs of one tenant never share a namespace with another's.
  • Usage. Cost is recorded per tenant, under its budget.
  • Rules. Its tool policy, the hosts its sandboxes may reach, its data rules and its retention, and the AI label on what its agents write for people.

A call that names an object of a tenant by its ID (a node, an API key, a join token, a rollout, a kill switch, a budget request, a receipt) gives a caller who may neither read that tenant nor act in it the answer it gives for an ID that names nothing: it says neither that the object exists nor which tenant holds it.

For a node, a key, a join token, a rollout or a kill switch, a member of the tenant who lacks the permission is told what they lack; a budget request and a receipt answer as not found to anyone who may not act on them, members of the tenant too.

Every cluster starts with the tenant default. Create more with:

Terminal
oz0 tenant create acme --display-name "Acme Inc."
oz0 tenant list

Most commands take --tenant, which defaults to default. Servers register a Temporal namespace for every tenant at start and every 30 seconds after.

The rest of a tenant's settings are tenant subcommands too:

Terminal
oz0 tenant budget acme 500                            # US dollars a calendar month
oz0 tenant egress acme api.github.com '*.pypi.org'    # hosts its sandboxes may reach
oz0 tenant review-widening acme second_admin          # who approves a widening of them
oz0 tenant tool-policy show --tenant acme              # what its tools may do
oz0 tenant ai-label show --tenant acme                # the AI label on what its agents write
oz0 tenant chat-titles acme model                     # chats titled by the agent's model, not the first message
oz0 tenant chat-read-notices acme on                  # owners see who read their chat through the audited access
oz0 tenant backends                                   # which Temporal backend each tenant is on
oz0 tenant move acme cloud                             # move it to another backend
oz0 tenant removed-account-keys acme finding          # keep the keys of people who leave, listed (default: revoke)

Widening what a tenant's sandboxes may reach is a reviewed change: a new egress entry, a plugin whose egress the tenant does not permit yet, or a credential binding that allows more waits as a change request until a tenant admin approves the delta they saw (oz0 change, Approvals to Changes).

The tenant's review-widening is confirm by default, under which the admin who asked may approve after seeing the delta, or second_admin, under which another admin must: regulated tenants should use it. Relaxing it back takes an owner. A new entry that reaches a host serving files anyone can publish (a code host's raw files, a package registry, an object store) says so on its finding, as a reason to look; it is approved like any other. See Reviewed changes.

A tenant's people, API keys and services can be grouped in teams, each with a quota of its own under the tenant's budget.

A conversation a person starts is their chat, private to them and those they share it with. Tenant admins change two settings, each audited:

  • chat-titles defaults to first-message, using the chat's first words as its title. With model, the agent's own model writes it in the first job: one small metered request, marked as written by AI. The edge never asks a model on its own.
  • chat-read-notices is on by default for now. The owner sees who read the chat through audited content access, when and why.

Accounts have a role in each tenant, and the permission to see what the tenant's jobs read and wrote is separate from the role. See Roles and permissions below and Console. Operator certificates, which the CLI uses, manage every tenant.

Roles and permissions

A role is a named set of permissions, and every API call needs one permission in the tenant it acts in. oz0 roles lists the roles in force; oz0 roles --permissions lists every permission with its level, the least of reader, operator, admin and owner that holds it.

Built-in roleWhat it may do
readerSee the tenant: nodes, agents, plugins, jobs and their steps' shape, cost, findings, usage, the inventory and governance settings
operatorAlso start and cancel jobs, decide approvals and answer questions, fire routines, call tools
adminAlso change the tenant: nodes, plugins, secrets, keys, the budget and governance; read its audit log and export evidence
ownerAlso decide who is a member of the tenant, with which roles
auditorWhat a reader sees, and the audit log, evidence exports, retention and held histories, settings and secrets' names; changes nothing
privacy-officerWhat a reader sees, and finding and erasing data subjects, retention and legal holds read, legal holds placed, incidents kept and the tenant exported; starts no job and installs nothing
agent-userUse the agents the membership's agent grant names in the chat drawer: see its own chats and jobs, cancel them, answer their questions, and decide an approval only where its deciders name the person

The four ordered roles each hold what the one before holds, exactly as before roles were sets. The permissions, by level:

  • reader: tenant.read, jobs.read (with jobs.read.own), feeds.read, chats.use, usage.read, inventory.read, governance.read, findings.read, budget.request.
  • operator: jobs.start (with jobs.start.granted), jobs.cancel (with jobs.cancel.own), jobs.manage, approvals.decide (with approvals.decide.named), questions.answer (with questions.answer.own), routines.run, tools.call, settings.read, candidates.write, evals.keep, payloads.encode, comparisons.start.
  • admin: audit.read, evidence.export, retention.read, retention.write, holds.place, subjects.find, subjects.erase, secrets.read, secrets.write, settings.write, plugins.install, nodes.manage, keys.manage, connections.manage, budget.manage, teams.manage, models.manage, governance.write, changes.review, inventory.write, notifications.manage, routines.manage, channels.manage, workspace.write, evals.manage, usage.export, setup.check, incidents.read, incidents.manage (with incidents.read), tenant.export.
  • owner: members.manage, signin.manage, governance.relax.
  • content.read is the membership's content permission, never part of a role.

A membership may hold up to four roles, separated by commas, and gets what any of them allows:

oz0 user grant ada --tenant acme --role operator                 # the content permission by default
oz0 user grant ida --tenant acme --role auditor                  # no content unless --content
oz0 user grant cleo --tenant acme --role agent-user --agents helpdesk,billing/*,tag:hr   # content by default: her own chats
oz0 tenant role-rules acme                                       # separation of duties

Agent grants. --agents names agents, every agent of a plugin (<plugin>/*), or every agent an agent definition tags (tag:<tag>, the agent definition's tags:). An agent user starts only those, and nothing without a grant; a grant on any other role narrows what it starts. The chat drawer's picker, Ask's hand-overs and StartJob all apply it.

An agent user sees only the jobs it started: another person's job, its approval's arguments, a turn continued from it and a conversation it did not start answer not found. Its membership has the content permission by default, since it reaches only its own chats and jobs; with --content=false it starts agents but cannot read their answers.

Separation of duties. A membership never holds auditor together with a role that changes the tenant (any admin-level or owner-level permission that does more than read), so an auditor stays independent of what it audits. A tenant's owners may allow it with oz0 tenant role-rules <tenant> --auditor-with-changes --reason ..., a governance change that takes a reason and, with four eyes, a second admin. A sign-in whose groups would give both keeps auditor alone; the audit log says so when the membership changes (membership.roles.conflict), and oz0 doctor lists the people kept at auditor (roles.sod_kept).

Role sets. A server started with --role-sets-file <path> or --role-sets-command <command> adds the roles of a role set to the built-in ones:

version: oz0-roles/v1
roles:
  - name: support
    description: Reads every job and cancels them
    permissions: [tenant.read, jobs.read, jobs.cancel, feeds.read, chats.use]

A role names permissions from the list above; it may not redefine a built-in role or name content.read.

The server reads the file or runs the command (without a shell, with --role-sets-timeout, 10 s by default, and a small environment of its own: give it what it needs as arguments) at start and every --role-sets-refresh (5 minutes), stores the last valid set, and every edge takes it from the registry. If the command fails, times out or prints something invalid, the last valid set stays in force and oz0 doctor says so (roles.source_failed).

A role that leaves the set, or a set that is taken away, leaves memberships naming it with the strongest of reader, operator, admin and owner whose permissions it held entirely (none if it held less than a reader's): never more than before.

Each change of the set is in the audit log (roles.set.changed), and oz0 doctor lists the memberships and group rules that still name a retired role (roles.fallback); a retired role nobody names any more is dropped.

In a cluster with several management servers, give every one the same --role-sets-file or --role-sets-command: a server started without it reads that as the set being taken away, and retires the custom roles until another server's next reading.

Nobody gives a role that holds more than they hold themselves, and a role with members.manage below the owner's also cannot change or remove a member who holds a permission it does not, give the content permission it lacks, or give an agent grant wider than its own.

The same holds for API keys: below the owner, a role with keys.manage makes no key whose role (reader, operator or admin) holds a permission it does not, and no key with content unless it has content itself. A tenant keeps its last owner: only a cluster admin removes or demotes it. The check runs in the transaction that changes the membership, with the tenant's members locked, so two owners demoting each other at once, through two management servers too, cannot both pass it.

Apps call the runtime API with a tenant's API keys (oz0 apikey create), each with one of the same roles; a key works in its own tenant and nowhere else, and a key scoped with --scope agent:<name>, flow:<name> or conversation:own works only on those resources.

What a key creates is labelled with it (created_via). A key belongs to the tenant and records who made it. When that person's account is removed, the tenant revokes the keys they made, unless its admins chose to keep them working as a finding with tenant removed-account-keys.

A tenant's owners make admin keys for tools that administer it, with a set of permissions they hold (and never more than they still hold) and an expiry of at most 90 days, and register apps that act as their people, each member narrowed to what the app may do.

On this page
Copyright © 2026