# Who can do what in your company

How organizations, workspaces and teams differ, what each role can do, and why a second company cannot read the first.

Source: https://tokentriage.com/docs/build/organizations-and-access

---
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';

Two departments share one company and one bill, and one of them handles employee records. The person
who deploys the support chatbot should not be able to read what HR's assistant was asked, and the HR
lead should not be able to change the support chatbot's model. Neither of them should have to ask you
to click Deploy for them.

That needs three ideas kept apart, and most tools conflate at least two of them: a container for
resources, a group of people, and a grant that connects the two. This page is those three ideas, the
eleven roles built on them, and what a denial looks like when one of them stops you.

It assumes you have a company already; [Create your company and first
application](/docs/get-started/first-company) is that path. It covers management access, not
spending caps. Caps are a separate mechanism, described in [The six limit
types](/docs/build/governance-limits).

## Three nouns that are not the same thing

Readers who skip this section reorganise their company twice.

An **organization** is your company. It owns the people, the workspaces and the billing
relationship. A **workspace** is where related resources and their isolation settings live:
environments, applications, LLM gateways and saved provider connections. A **team** is a group of
people inside the organization, and nothing else.

A workspace and a team can carry the same name and still have no relationship. Changing a team's
roster moves no application data, and moving an application between workspaces changes no roster.
Small companies keep one workspace and delegate at the application level instead; separate
workspaces are for the case where two groups' resources should not sit side by side, such as HR
records beside a support queue.

Containment runs in one line: an organization contains workspaces, a workspace contains
applications, an application contains gateways. One sentence about grants holds at every depth:

> A grant made at a scope applies to that scope and to everything inside it, and to nothing above it
> or beside it.

So a grant on a workspace reaches all of its applications and their gateways. A grant on one
application reaches that application's gateways and stops there. It cannot be widened by the person
who holds it.

## The eleven roles, and what each one can do

A closed list of roles is the part a lead has to be able to read out loud to the person they're
granting it to, so plain capability is the primary description. The role IDs are the strings the API
and the audit trail use.

| Role | What the person can do | Where you can grant it |
|---|---|---|
| `org_owner` | run the company: rename it, manage people, delegate access, read and change billing, and list every workspace, application and gateway | organization |
| `org_admin` | everything the owner can do except read or change billing | organization |
| `billing_admin` | read and change billing, and see that the company exists | organization |
| `workspace_admin` | run one workspace: manage its applications and gateways, edit configuration, deploy, manage keys and provider connections, run tests, and delegate inside the workspace | workspace |
| `app_developer` | edit one application's gateway configuration, run tests, use an allowed provider connection, and see what its traffic cost | application |
| `env_deployer` | deploy to the environments the grant lists, and nothing else | application, limited to named environments |
| `analyst` | read a workspace's or application's metadata and its spend: cost, traffic, reliability, quality evidence | workspace, application |
| `content_reader` | read request and response content for the scope granted | workspace, application, gateway |
| `credential_manager` | create and revoke TokenTriage keys | workspace, application |
| `connection_manager` | add, test, rotate and disable saved provider connections, and allow their use | workspace |
| `runtime_service` | send traffic: invoke inference and write feedback in the environments the grant lists | application or gateway, limited to named environments |

Two consequences are worth saying out loud. An `org_owner` binding satisfies a read of any
application in the company, and it carries no ability to edit configuration, deploy, read request
content or send traffic. Reading request content is an explicit, audited self-grant even for the
owner, so an account takeover does not come with a transcript of your customers' prompts.

Role versions are integers, and a new version of a role cannot widen bindings that already exist.
That is why a capability you granted last month still means what you read when you granted it.

<Details summary="Advanced: the action strings behind each role">

Authorization is a check against a closed list of actions. These are the strings the API, the audit
trail and a preview's advanced view use; they are not how the grant is presented to the person
receiving it.

| Role | Actions |
|---|---|
| `org_owner` | `org.read`, `org.manage`, `members.manage`, `access.delegate`, `billing.read`, `billing.manage`, `workspace.read`, `workspace.manage`, `app.read`, `app.manage`, `gateway.read`, `metadata.read`, `cost.read` |
| `org_admin` | as `org_owner`, without `billing.read` and `billing.manage` |
| `billing_admin` | `org.read`, `billing.read`, `billing.manage`, `cost.read` |
| `workspace_admin` | `workspace.read`, `workspace.manage`, `app.read`, `app.manage`, `gateway.read`, `config.edit`, `deploy`, `keys.manage`, `connections.manage`, `connections.use`, `tests.run`, `metadata.read`, `cost.read`, `access.delegate` |
| `app_developer` | `app.read`, `gateway.read`, `config.edit`, `tests.run`, `connections.use`, `metadata.read`, `cost.read` |
| `env_deployer` | `deploy`, plus `deploy.protected` only when the binding lists a protected environment |
| `analyst` | `metadata.read`, `cost.read`, `app.read`, `gateway.read` |
| `content_reader` | `content.read` |
| `credential_manager` | `keys.manage` |
| `connection_manager` | `connections.manage`, `connections.use` |
| `runtime_service` | `inference.invoke`, `feedback.write` |

`cost.read` is the newest verb in that closed set, and six of the eleven roles hold it:
`org_owner`, `org_admin`, `billing_admin`, `workspace_admin`, `app_developer` and
`analyst`. The five it is withheld from are `content_reader`, `credential_manager`,
`connection_manager`, `runtime_service` and `env_deployer`. Reading a transcript, holding
keys, managing connections, sending traffic and deploying are five jobs that do not need
to know what anything cost.

</Details>

## Protected environments, and the one modifier on a grant

Production needs a stronger grant than Development, and naming a thing "production" cannot be what
provides it.

Two roles carry a list of environments: `env_deployer` and `runtime_service`. A binding without that
list reaches no environment, and a binding with it reaches those and no others. Adding a new
environment to a workspace grants nobody anything.

`deploy.protected` is added to an `env_deployer` binding only when the binding names a protected
environment. Production is protected because a field on its row says so, so renaming Development to
"Production" confers nothing, and renaming the protected environment doesn't remove its protection.

## What an invitation shows before you send it

An invitation is where a lead either understands the grant or guesses at it, so the review screen
states the grant in sentences rather than in action strings.

```text
Invite IT Lead to Acme

Access: Manage User Portal
Can: configure gateways, deploy within granted environments,
     manage application access within the delegated scope
HR Portal: no access
Organization billing: no access
Request content: not included unless separately granted

Send invitation
```

That preview is the intended copy from the design contract, not a capture from a running screen.
Read the negative lines: they are there because "manage this application" is normally heard as more
than it grants. The equivalent action strings stay behind an advanced view for the reader who wants
them.

An invitation stores a SHA-256 hash of its token rather than the token, and expires within seven
days of being sent. Accepting one revalidates three things at that moment: that the inviter still
holds delegation over the scope, that the invitation is unexpired, and the precise role versions it
named. An invitation written by somebody who has since lost their own access grants nothing.

### Who may send which invitation

An invitation does one of two things, and the two are different decisions with different
authorities. Offering access to somebody who is **already in the company** widens a grant, and takes
`access.delegate` at the scope being granted — the `workspace_admin` lead of IT can do it for IT
without asking anybody. An invitation to an address that is **not a member yet** enlarges the
company, because redeeming it writes an organization membership, and that takes `members.manage` at
the organization: `org_owner` and `org_admin` hold it, and no other role does.

The split follows removal. `members.manage` is also what takes a person off the roster, so without
it a lead could add somebody they would then be unable to remove.

A lead who tries gets a `403` whose detail names the action rather than the role:

```text
403 forbidden
members.manage is required to invite somebody who is not a member of this
organization yet
```

There are two ways past it: ask an organization owner or administrator to send that invitation, or grant
the access to a colleague who is already a member.

**This check fails closed.** Deciding whether an address is already a member is a lookup against the
company's own roster. A deployment that cannot answer it — no account directory composed, or a
directory that does not respond — treats the invitation as one that would add a member, so it asks
for `members.manage` rather than assuming the invitee is already here.

Teams, invitation previews and explicit role bindings are available in **People and access**.
The preview names the offered role and scope; acceptance checks them again against the inviter's
current authority. Joining a team does not grant the team's management rights.

## Traffic-only access is its own category

The person who installs a key into a running service is the one most often over-granted, because
"they need access to the app" gets read as portal access.

A runtime maintainer installs a TokenTriage runtime key into your application and needs nothing
else. The key belongs to the application's service identity, not to a powerful human account.
`runtime_service` grants two things: invoke inference, and write feedback, in the environments its
binding lists. It grants no resource management and no request-data reads.

Putting that person on a team roster does not give them the team's management rights, because rights
come from bindings and rosters are not bindings. If they also hold a login, the people screen tells
them they hold no management access, instead of showing them an application dashboard of forbidden
panels.

That key holds no provider secret either. Applications authenticate to TokenTriage with a
TokenTriage runtime key, and TokenTriage reaches the provider through a saved connection. [Add a
saved provider connection](/docs/build/provider-connections) is how that key gets there.

Replacing a runtime key needs the same `keys.manage` grant and nothing more, because a replacement
keeps the service identity and the allowlist it already had: [Rotate a runtime
key](/docs/build/rotate-a-runtime-key).

## A management key carries a person, and the list says which

A management key is what a script or the CLI authenticates with, and it is not a separate identity
with permissions of its own. It acts as the person who created it, bounded by that person's current
access, inside one company, and it stops working when it is revoked, when it expires, or when that
person leaves. It cannot create a company or a workspace, or accept an invitation, because those
operations name no organization.

So the company's key list answers the question an incident starts with: whose authority is this key
spending? Each row names the person it acts as, saying "you" only when the key is the reader's own,
and a revoked row names who revoked it. Requests made with a key are attributed to that person with
a key marker in the audit trail, so automation stays distinguishable from a browser session.

A script presents that management key in `TOKENTRIAGE_MANAGEMENT_KEY`, without a browser cookie.
For example, an organization administrator can read the first roster page with the CLI:

```console
tokentriage cloud --base-url "$TOKENTRIAGE_BASE_URL" --org "$TOKENTRIAGE_ORG" --json team list
tokentriage cloud --base-url "$TOKENTRIAGE_BASE_URL" --org "$TOKENTRIAGE_ORG" --json member list
```

If the response has `next_page_token`, continue with `--page-token` before treating its rows as the
whole roster. The Python roster example (`sdk/python/examples/read_team_rosters.py`)
and TypeScript roster example (`sdk/ts/examples/read_team_rosters.ts`)
follow every page and read each team's membership. With the same three environment variables set,
their commands are:

```console
python sdk/python/examples/read_team_rosters.py
npx tsx sdk/ts/examples/read_team_rosters.ts
```

The report's company count counts each person once. Each team count describes that team's roster,
so a person in both IT and HR appears in both counts. Adding those counts does not give the company
headcount, and none of these counts says what content a person may read. The examples print roster
metadata only; they do not grant roles, send invitations or request browser cookies.

## Content, cost, billing and metadata are four separate grants

The most expensive assumption in a permissions model is that one of these implies another.

Four verbs, four answers. `metadata.read` says a request happened and how long it took.
`cost.read` says what the traffic cost, per gateway, model, application and person. `content.read`
is the prompt and the completion. `billing.read` and `billing.manage` are what the company is
charged. No role bundles all four.

`metadata.read` used to carry the spend figure with it, and it no longer does. Splitting them is
what lets a lead grant somebody latency and error rates without granting them the bill, and it is
why an invitation preview for an `app_developer` names the two separately.

So `analyst` reads what a gateway cost and cannot read what anybody asked it. `content_reader` reads
bodies for one scope and carries no other listing rights, and no cost figure at all. `billing_admin`
reads the invoice and learns nothing about the traffic behind it beyond its cost. Shared team
membership grants no content access on its own, which is why one of Acme's shared developers can
work in both workspaces without reading either's transcripts.

## Why a second company cannot read the first

One login belonging to two companies is the case where a leak would be quietest, so the resolution
order is worth knowing.

Every request resolves in four steps, in this order:

1. Confirm active membership in the organization named in the URL. No membership, no further steps.
2. Take the union of the subject's direct and team bindings whose scope is the target or one of its
   ancestors.
3. Intersect that with the binding's environment restriction, where it has one.
4. Check the action the request needs against what remains.

A foreign ID confers nothing at any step, and step 1 means an ID from another company is not even
considered. Here is the denial that follows.

An `app_developer` in Acme pastes an application ID belonging to another company. The answer is
`404`, not `403`, even though the application exists: a `403` would confirm that it does. The same
developer asking to deploy to Acme's own Production without an `env_deployer` grant naming it gets
`403`, because that scope is theirs and the action is not.

| What happened | Status |
|---|---|
| no session, or an unverified one | `401` |
| the scope is unknown, or belongs to another organization | `404` |
| the scope is yours and your roles don't carry the action | `403` |
| the state conflicts, such as a slug already taken | `409` |
| the revision you held is stale | `412` |
| the combination isn't supported | `422` |

Every one of those answers is a problem document carrying a `request_id`, which is the value to
quote when you ask why.

## How long a revocation takes

A permissions page that doesn't answer this has left the reader guessing at the number that matters
during an incident.

Each membership change, binding change and invitation acceptance increments the organization's
authority epoch. Authorization caches key on the subject, the organization and that epoch, and hold
for at most 20 seconds. A revocation therefore reaches every reader within that bound, rather than
waiting for a session to end.

That is the bound for management reads. Stopping provider dispatch after a connection change is a
separate bound with its own measurement, on the provider connections page.

## What this page doesn't cover

- Spending caps, rate limits and how a limit counts: [The six limit
  types](/docs/build/governance-limits).
- Roles in the self-hosted dashboard, which are a different set: [Roles and
  permissions](/docs/build/dashboard-roles).
- Who may add a provider key, and how it is stored: [Add a saved provider
  connection](/docs/build/provider-connections).

<NextSteps items={[
  { title: 'Create your company and first application', href: '/docs/get-started/first-company', description: 'The path that creates the organization, workspace and first gateway.' },
  { title: 'Add a saved provider connection', href: '/docs/build/provider-connections', description: 'Where a provider key lives, and who may bind it to a model.' },
  { title: 'The six limit types', href: '/docs/build/governance-limits', description: 'Spending and rate caps, which are separate from these grants.' },
  { title: 'Export your data and delete a workspace', href: '/docs/build/export-and-delete', description: 'The two lifecycle verbs, and what a deletion purges and keeps.' }
]} />
