Users and Access
Main Sequence is built for shared work. CodeRepositories, datasets, application surfaces, constants, secrets, artifacts, and releases are rarely useful for only one person.
That means access control is not an optional admin topic. It is part of how teams build and operate on the platform.
This guide explains the access model in plain language, without implementation details.
For notification delivery rules and client usage, see Notifications.
To read the policies the platform defines for each kind of shareable object, with what each one allows, see Access Policy Catalog. Teams are controlled by policies; see Teams and Who Has Access to an Object.
Start with the right mental model
There are five concepts to keep separate:
Organization: the tenant boundaryUser: a person who signs in to the platform, or a workload identity: the User a deployed Job, FastAPI release or Agent runs as, or an external workload calls asRole: a broad level of responsibility inside the organizationTeam: a reusable group that can hold policies on resources- Resource access: the policies that decide who can do what on a specific resource
Most access questions can be reduced to:
- which organization does this belong to
- who is the user
- does the user have a broad admin role
- does the user hold a policy on the resource directly
- does a team the user belongs to hold a policy on the resource
How the pieces fit together
graph TD
Org["Organization"] --> Users["Users"]
Org --> Teams["Teams"]
Users -->|membership| Teams
Users -->|direct access| Resources["CodeRepositories, Constants, Secrets, Buckets, Artifacts, Agents, Releases, Jobs"]
Teams -->|team access| Resources
Organization
The organization is the main security boundary.
In practice, that means:
- users belong to one organization
- teams belong to one organization
- resources are usually visible only inside that organization
- access is designed around collaboration inside the same tenant, not across tenants
This is why the first access question is usually not "what role does this person have?" but "are they even in the same organization?"
User
A user is the person operating on the platform.
Users are the identities that:
- sign in
- create resources
- run commands through the CLI
- receive direct access to resources
- belong to one or more teams
When you give a user a policy on a resource directly, you are saying:
this specific person should be able to do what this policy allows on this specific thing
That is the most explicit form of access.
Workload identities
A deployed Job, FastAPI release or Agent runs as its own User: its workload identity. A program outside Main Sequence can have one too, as an external workload. A workload identity belongs to the Organization like a person, but it is not a person: other Users see no username, email, join date or first and last name for it.
Any member of the Organization can read a workload identity by its UID, for example to check the caller of a request an application received:
from mainsequence.client import User
user = User.get_by_uid(user_uid)
user.identity_type # "workload"
user.is_active
user.job_uid # the Job it belongs to, or None
user.resource_release_uid # the release it belongs to, or None
user.agent_uid # the Agent it belongs to, or None
user.username, user.email # None for a workload identity
For a workload identity the platform sends uid, identity_type, is_active,
job_uid, resource_release_uid and agent_uid. The User fields it does not
send are None, or empty for lists.
To go the other way, from a workload to its identity, read
workload_user_uid on the Job, ResourceRelease or Agent. It is None
when the object has no workload identity, for example a static-site release,
a release's backing Job, or an Agent without a runtime release. An Agent and
its runtime release report the same value.
from mainsequence.client import Job, User
job = Job.filter(name__contains="nightly-prices")[0]
user = User.get_by_uid(job.workload_user_uid) # identity_type == "workload"
User.filter() lists people only. Ask for workload identities with the
identity_type filter; the listing holds the ones you can view:
workloads = User.filter(identity_type="workload")
Both listed rows and UID lookups carry workload_name, the name of the Job,
release or Agent, when you can view the workload. It is None for a workload
you cannot view, for a person, or when the response does not carry it.
identity_type is one of human, service_account, deleted_user and
workload. A person's row may not carry it, so test for "workload" rather
than for "human".
Inside a FastAPI request, an application can read the directory as its caller
sees it with reads_as_caller(): User.filter(search=...) then finds people by
email or name and workloads by their Job, release or Agent name, and each
workload row carries managed_by_caller. See
Reading the directory as the caller.
The access methods take a workload identity like any other User. An application that received a request from a workload can give that workload a policy on its own objects, and the platform decides whether the grant is allowed:
caller = User.get_logged_user()
user = User.get_by_uid(caller.uid)
if user.identity_type == "workload":
# any object the application created, here an Artifact
artifact.attach_policy("viewer", principal_type="user", principal_uid=user)
External workloads
An external workload is a program that Main Sequence did not deploy and that calls it with its own identity: a service in your cloud, a CI job, a scheduler. Its User is a workload identity like a Job's or a release's: it takes no seat and has no password, login or API token.
- Organization admins create it, in one Environment. It acts in that
Environment only: a request it makes for another Environment is refused with
403. Another Environment needs another external workload. - It signs in by federation. An Organization admin adds a federated credential to it, naming the identity provider's issuer, the exact subject of the program's identity, an audience dedicated to Main Sequence and, optionally, claim conditions. The program presents a token from its own identity provider and receives a Main Sequence access token that lasts 900 seconds and cannot be refreshed. No secret is issued, stored or shipped with the program. Only Organization admins add, disable and enable credentials. The SDK side is described in Workload Identity Auth.
- Its access is its policies. It starts with none. The external workload
itself is shared through its own policies,
viewer,editorandowner, held by people and Teams. Aneditormanages the workload's User: they give it the policies it needs elsewhere, for exampleuseron a release, under the ordinary rules for giving access. - It never acts for a person.
acts_for_requestercannot be enabled for an external workload. - Stopping it. Disabling a credential is immediate and reversible: exchanges stop, and an access token already issued fails at its next request. Deactivating the external workload is final; a replacement is a new external workload, with a new User and none of the old access.
A call from an external workload reaches a FastAPI release like any other
call: the caller assertion names the workload's User, so
User.get_logged_user().uid is that User's UID and is_organization_admin is
false. User.get_by_uid(uid) reads it with identity_type "workload" and
with job_uid, resource_release_uid and agent_uid all None. An
application that an external workload calls recognizes it by that UID, read
from a call the workload made, never by a name.
An external workload that serves its own users can send their context with a
call, in the W3C baggage header. Its User says who is calling; the context
says who the call is for, and is believed only as far as the application trusts
that caller. See Caller context.
Applications that act for their requester
An application's own access comes from the policies its workload User holds: give that User policies on objects as you would a person. Those policies cover every call the application makes for itself.
An application can instead be enabled to act for its requester, the person
whose own request it is serving, with the platform setting
acts_for_requester:
- It is
falseby default. Nothing changes for an application that leaves it off. - Only Organization admins enable it: after the application exists, with
PATCH /api/v1/workload-users/<workload_user_uid>/, or in code, by declaringacts_for_requester: trueon the resource in the repository workflow file. A declaration turns it on only when the person who pushed it is an Organization admin; otherwise that resource fails before it deploys. A push made by a bot right after an admin's push, for example a GitHub Actions release job that commits the next version, keeps that admin's authority when it leaves the workflow file unchanged. Once the declaration has turned it on, later pushes of the same declaration keep it on, whoever pushes. - An application that should act only for people, for example a data analyst Agent, holds no grants of its own.
The platform applies one rule to every call an Agent makes for its work:
| Request | Permissions used |
|---|---|
| No delegation | The caller's own ordinary permissions: a person, or the calling workload |
| Valid delegation | The person's ordinary permissions, including administrative ones |
| Invalid or expired delegation | None: the call is rejected, never switched to another identity |
Requester-bound access covers the reads, creates, updates, runs, access changes and deletes the person may make through supported operations. Every operation is checked against that person, never against the acting Agent's grants, and the two are never combined.
The access lasts at most 24 hours after the person's original request, and only while the turn or Task serving it runs. The platform checks it again on every call and refuses operations that do not support requester-bound access.
Platform-authorized Agent-to-Agent delegation keeps the person who started the work and the original request time. Each delegated Agent must itself be enabled to act for its requester; delegation cannot select another person, increase their permissions or restart the 24-hour limit. A receiving application cannot forward its inbound assertion to create another requester binding.
The platform decides which operations support requester-bound access. Updating the SDK does not enable an operation or turn a refused call into the application's own call.
Nobody names the requester in a request. An application never accepts a
person's UID from a request body, header or query parameter: the person comes
only from the platform. An application that receives a requester-bound call
reads the requester with User.get_requester(), the person's own uid and
is_organization_admin, asks about the person's teams with in_teams(), and
checks that person's permission
for the exact object and operation, for reads and writes alike. Each handler or
tool decides whether it needs a person; see
Requester-bound calls.
What people are told
Every client that shows an application with acts_for_requester shows the
platform's statement, exactly:
This Agent works with your identity. It can read, create, change, run, share or delete only what your permissions allow through supported operations, only while serving your request, and for at most 24 hours after you ask. It uses your ordinary permissions, including administrative permissions, and access is checked on every call. Your Organization's administrator approved it to work this way.
Do not paraphrase or shorten it, and do not describe such an Agent as read-only or promise that it cannot change, share or delete anything.
The plain risk: a model-driven Agent can be steered by prompt injection. Its reads and writes can reach as far as the person's ordinary permissions allow, administrative ones included, such as changing who has access to accessible objects or deleting them through supported operations, for at most 24 hours after the original request. A read grant does not authorize a write. This is why only Organization admins decide which Agents may act for people, and why every operation still needs its own authorization check.
Roles
Roles are the broad, organization-level classification of what kind of platform user someone is.
For most practical workflows, the important roles are:
Organization AdminDev UserLight User
Use roles to think about broad responsibility:
- who can administer the organization
- who is expected to build and maintain workflows
- who mostly consumes outputs
Do not use roles as your mental model for day-to-day access.
Why:
- roles are coarse
- real collaboration usually happens at the resource level
- two users with the same broad role may still need access to very different CodeRepositories, tables, or application surfaces
So the safe rule is:
- roles define broad responsibility
- the policies on each resource define actual resource access
Teams
Teams are reusable groups of people and workload identities inside one organization.
Use a team when the same set of people should repeatedly receive access to the same class of resources.
Examples:
ResearchPlatformRiskExecutionClient Reporting
Teams are useful because they let you give access once and reuse that decision many times.
Instead of giving five people policies on several application resources and datasets one by one, you can give those policies to one team and manage membership there.
Team members and creators can be people or workload identities. Their summaries
carry uid and, when supplied, member_kind ("person" or "workload"). For
workloads, username, email, first_name and last_name are None when
omitted or null in the response. Use uid for identity, not personal fields.
Membership and control
A team has two separate things:
- Membership is who is in the team: who receives the policies the team holds.
- Control is the policies on the team, which decide who can see it, change its members, edit it or delete it.
Any person can create a team, and the creator becomes its owner and its first member. The policies on a team are:
| Policy | What its holder can do |
|---|---|
viewer |
See the team and who is in it |
membership_manager |
See the team and its members, add people and workload identities, and remove members |
editor |
See the team and its members, rename it, change its description, activate or deactivate it, and see its policies |
owner |
Everything on the team, including deleting it and deciding who manages it |
member |
Held by every member and never attached: see the team and its members, and leave it |
Controlling a team does not require being a member, and being a member gives no
control beyond member. Organization admins hold every action on every team of
their organization. Each policy's full description is in the
Access Policy Catalog:
mainsequence access policies --type organization-teams.
The built-in full-directory team is computed: its members are every active
person of the organization. Nobody adds, removes or leaves its members, edits
or deletes it, and only Organization admins give its viewer policy, which
shows every active team of the organization and its members.
Manage members in Python
from mainsequence.client import Team
team = Team.create(name="Research", description="Model validation")
team.allowed_actions # your actions on the team: every one, as its owner
team.list_member_candidates(search="ana")
team.add_member(user_uid)
team.list_members()
team.remove_member(user_uid)
team.list_held_access() # the policies the team holds on other objects
team.leave()
| Method | Route | Returns |
|---|---|---|
Team.create(name=..., description="", is_active=True) |
POST /api/v1/teams/ |
the new Team, with members and allowed_actions |
list_members() |
GET /api/v1/teams/{uid}/members/ |
list[UserSummary]: people first, then workload identities |
add_member(user_uid) |
POST /api/v1/teams/{uid}/members/ |
the member's UserSummary; adding a member again changes nothing |
remove_member(user_uid) |
DELETE /api/v1/teams/{uid}/members/{user_uid}/ |
None |
leave() |
POST /api/v1/teams/{uid}/leave/ |
None |
list_member_candidates(identity_type=None, search=None) |
GET /api/v1/teams/{uid}/member-candidates/ |
list[UserSummary]: who you may add and is not a member yet, at most 200 |
list_held_access() |
GET /api/v1/teams/{uid}/held-access/ |
list[TeamHeldAccess]: each policy the team holds on an object you can read |
Every method also takes timeout, and a user_uid can be a UID or an object or
a dict that carries it as uid, such as a User.
Team.get_by_uid()andTeam.create()return a team's detail, which carriesallowed_actions, the ids of your actions on it such asorganization-teams.update. Listed teams carryNone.- Adding needs the team's
add_memberaction. An Organization admin can add any person or workload identity of the organization; anyone else can add people who share an active team with them, themselves, and the workload identities they can view. Anyone else raisesBadRequestErrorwithcodeinvalid_user_uids. - Removing needs
remove_member, orleaveto remove yourself. Removing someone who is not a member raisesNotFoundError. - Leaving ends your membership only: a policy you hold on the team, such as
owner, stays. Leaving a team you are not a member of raisesBadRequestErrorwithcodenot_a_team_member. list_member_candidates()lists people by default;identity_type="workload"lists workload identities, andsearchmatches an email or a name.- A
TeamHeldAccessrow has thebinding_uid, the object'sresource_type,object_uidandobject_name, thepolicyid the team holds, itssource,granted_by(anAccessPrincipal) andcreated_at: what joining the team gives a member.
Decide who controls a team
The policies on a team are read and changed with the access methods every object with access routes has:
team.get_access() # bindings, and the computed access of admins and members
team.attach_policy("membership_manager", principal_type="user", principal_uid=user_uid)
team.detach_policy(binding_uid)
team.explain_access("organization-teams.update", principal_uid=user_uid)
See Who Has Access to an Object for every method, the
models and the errors. A refused request raises PermissionDeniedError, whose
payload names the required_action and the granting_policies.
From the command line
mainsequence organization teams create Research --description "Model validation"
mainsequence organization teams members list <TEAM_UID>
mainsequence organization teams members add <TEAM_UID> <USER_UID>
mainsequence organization teams members remove <TEAM_UID> <USER_UID>
mainsequence organization teams leave <TEAM_UID>
mainsequence organization teams access list <TEAM_UID>
mainsequence organization teams access grant <TEAM_UID> membership_manager --user <USER_UID>
mainsequence organization teams access revoke <TEAM_UID> <BINDING_UID>
mainsequence organization teams access explain <TEAM_UID> organization-teams.update --user <USER_UID>
See Teams in the CLI reference.
What team membership means
If a user belongs to a team, they inherit the policies that team holds on resources.
That is the core idea.
Membership means:
- the user is part of that team for access purposes
- the user receives inherited access through the policies the team holds
- the user holds the team's
memberpolicy: they can see the team and its members, and leave it
Membership does not automatically mean:
- the user can manage the team
- the user can change who else is in the team
- the user can change who controls the team
- the user can administer every resource the team can see
That distinction matters a lot.
Being inside a team means the user benefits from the team's access. Managing the team itself comes from the policies on it.
Resource access
Main Sequence decides access to every shareable resource with policies.
That is the practical layer that answers:
- who can see or use this resource
- who can change it
- who decides who has access
Each kind of resource has its own policies, from narrow ones for consumers to
owner, which also decides who has access. A policy goes to a person, a
workload identity or a team on one resource; see
Who Has Access to an Object and the
Access Policy Catalog:
- a constant's
reader,editorandowner, a secret'sreader,value_reader,editorandowner, and a bucket'sreader,contributor,editorandowner; see Who can use a constant, Who can use a secret and Who can use a bucket; - an Artifact's
viewer,editorandowner; see Who can use an Artifact; - a code repository's, a branch's and a Project's
reader,editorandowner, where a repository's or a Project's policies pass down to its branches, and using a repository or a branch also needs the platform'suserproduct role; see Code Repositories, Branches and Projects; - an Agent's
viewer,user,operator,editorandowner; see Agents; - a release's
viewer,user,editorandownerand a Job'sviewer,runner,editorandowner, which also take the policies of the branch that declares them, or of the Agent or the release they back; see Who can use a release and Who can use a Job; - a model-provider credential's and a custom model provider's
user,editorandowner, which decide who sends requests through them and who maintains them.
The same pattern shows up again and again:
- some people can see or use a resource
- some people can maintain it
- few people decide who has access
- access may come directly, through a team, or from a resource above it
Direct access vs team access
There are two common ways a user gets access to a resource.
Direct access
A policy on the resource goes straight to a user.
Use this when:
- the access is personal
- the resource is unusual or one-off
- you do not want to create or reuse a team for it
Team access
A policy on the resource goes to a team, and all current team members inherit that access.
Use this when:
- several people need the same access
- access should stay aligned as people join or leave the team
- you want one reusable access boundary instead of many manual grants
Consumers vs maintainers
Most of the time, the practical distinction is simple:
- consumers get the narrowest policy that lets them inspect or use the
resource, such as
reader,vieweroruser - maintainers get
editor, which also changes and deletes it - only the people who decide who has access get
owner
For example:
- on a release,
usercalls the application without seeing its logs, andvieweralso reads them - on an Artifact,
editorreplaces or deletes its file
This is the cleanest engineering split:
- consumers get a reading or using policy
- maintainers get
editor
The subtle but important rule about teams
This is the part people most often get wrong:
a team can hold policies on resources, but being a member of the team does not automatically make you an administrator of the team itself
In other words:
- if
Researchholds a policy on a dataset, members ofResearchinherit access to that dataset - that does not automatically mean every member of
Researchcan manage theResearchteam
This is why "team membership" and "team administration" should be thought of as separate concerns: membership is who is in the team, and administration is the policies on it.
Practical examples
Example 1: Give a team access to a dataset
If Research holds viewer on an Artifact:
- current members of
Researchcan read the dataset - future members of
Researchwill also inherit that read access - removing someone from
Researchremoves that inherited path
Example 2: Let one person maintain a dataset directly
If one workflow maintainer needs to manage a dataset directly:
- attach
editoron theArtifactto that user - do not widen access for the whole team unless that is actually intended
Example 3: Team membership is not team administration
If Alice is a member of Research:
- Alice inherits the access
Researchholds on resources - Alice can see
Researchand its members, and leave it - changing who is in
Researchneeds itsmembership_managerorownerpolicy - renaming or deactivating
Researchneeds itseditororownerpolicy, and deciding who controls it needsowner
Example 4: A team that maintains a resource does not have unlimited control
If Team A holds editor on a release or a dataset:
- members of
Team Acan change and delete that resource - that does not automatically mean they can change every access rule everywhere else on the platform
Keep your thinking resource by resource.
How access fits the platform workflow
The same resource-scoped pattern applies across the platform:
CodeRepository, its branches and Projects control access to code and to what runs from itConstantandSecretcontrol access to runtime configurationBucketandArtifactcontrol access to stored filesAgentcontrols who talks to an Agent and gives it TasksResourceReleasecontrols access to supported deployed experiences such as APIsJobcontrols who runs scheduled work and reads its runs
That is why RBAC appears early in the Main Sequence workflow. The moment a resource becomes useful to other people, access design becomes part of the engineering work.
Rules of thumb
- start with the organization boundary first
- use roles for broad responsibility, not detailed resource access
- use teams for repeatable access patterns
- give access directly for exceptional or personal cases
- give the narrowest policy that does the job, and
owneronly to those who decide who has access - do not assume team membership means team administration
- think in terms of resource boundaries: CodeRepository, dataset, secret, bucket, artifact, Agent, release, Job
For related configuration guidance, see Constants and Secrets.
Identity facts for application authorization
User detail and current-user responses may supply is_organization_admin
(a strict boolean) and active_team_uids (active Team UUIDs). They are facts
computed by the platform; the SDK does not infer roles from group names or
implement an application's resource grants. Missing fields remain None, so
applications requiring these facts must fail closed rather than assume access.
Use User.get_authenticated_user_details() for the process user and
User.get_by_uid(uid) for an already verified application caller, subject to the
existing platform directory permissions. These are ordinary User reads; they do
not change the process identity or caller assertion contract. Refresh facts at
the application's authorization boundary to apply Team/admin changes.
The caller can be a person or a workload identity; see Workload identities for what the lookup returns for a workload.