Permissions
Two gates, deliberately not merged.
Gate::define('admin', fn (User $user) => (bool) $user->is_admin);
Gate::define('administer-workspace', function (User $user, ?Workspace $workspace = null) {
$workspace ??= $user->currentWorkspace();
return $user->roleIn($workspace)?->canManageMembers() === true;
});
Why two
admin is the operator of Félagi itself, across every workspace. administer-workspace is an
owner or admin inside one workspace.
Collapsing them would mean a customer could only manage their own agents by being handed access to every other customer's data. A test locks the separation:
it does not grant workspace administration to a platform admin from outside — an operator has
adminbut notadminister-workspacein a workspace they are not a member of.
Roles inside a workspace
| Role | Can |
|---|---|
| Member | Work on issues: create, comment, assign, mention agents, run existing agents |
| Developer | Everything a member can, plus hire agents, connect machines, write skills and build autopilots |
| Admin | Everything a developer can, plus manage people and workspace settings |
| Owner | Everything an admin can, plus delete the workspace |
Developer exists because running the machines and deciding who works here are different kinds of trust. A contractor can be handed the agent workforce without also being handed the member list; a team forced to merge the two ends up giving everyone everything.
Two gates carry the split:
| Gate | Roles | Covers |
|---|---|---|
administer-agents |
owner, admin, developer | Agents, runtimes, skills, autopilots |
administer-workspace |
owner, admin | Members, workspace settings, webhooks, import, cycles |
write-knowledge |
every member | Knowledge base articles, whiteboards |
Navigation follows the gates rather than guessing: a developer is never shown a link that answers 403.
Two invariants are enforced when roles change:
- A workspace cannot lose its last owner, by demotion or removal
- Nobody can remove themselves, which would lock them out of their own workspace
Removing a membership never deletes the account or anything the person wrote.
Where each area lives
The widest gate in the product
write-knowledge grants to anybody who works here, which makes it look pointless — the check that
matters is membership of the workspace, and every role passes it.
It is deliberate. A knowledge base only admins may write to is a knowledge base nobody writes to, and the failure mode of documentation is always that it was never written rather than that the wrong person wrote it. The same is true of a whiteboard: the early, messy phase of a project does not survive being gated.
Two narrower rules live on the records themselves rather than in a gate, because they are about what a deletion takes with it:
| Action | Who |
|---|---|
| Delete an article | Its author, or a workspace admin — a section takes its children |
| Delete a whiteboard | Its author, or a workspace admin |
| Change who sees a whiteboard | Its author alone. The setting is about their comfort |
| Edit or delete a time entry | Its author alone. Somebody else's hours are their statement |
| Edit an agent's time entry | Nobody. The run is the record |
| Prefix | Gate | Contents |
|---|---|---|
/app |
authenticated member | Dashboard, projects, issues, agent roster, settings |
/admin |
administer-workspace |
Members, agents, runtimes, skills, workspace settings |
/system |
admin |
All users, analytics, system information |
The agent roster stays in /app and is readable by every member. Seeing who is on the team is
part of the product, the same way the member list is; only hiring, editing and archiving are
administration.
Joining a workspace
Everybody is invited; nobody is inserted. An administrator sends an invitation to an email address, and nothing happens to any account until the recipient accepts it themselves.
That is a deliberate reversal of the obvious design. Creating an account for somebody means choosing their first password, which means knowing it — and a colleague should not have to trust that.
- Invitation tokens are stored hashed and expire after 14 days
- Re-inviting replaces the open invitation and invalidates the previous link
- An address that already has an account joins with one click; a new one is created on the way in
- The address is fixed by the invitation, never by the form. Otherwise the link is a free pass to a pre-verified account at any address
- Accepting marks the address verified, because the link reached it
- Being signed in as somebody else does not accept on their behalf — a forwarded invitation must not put the wrong person in a workspace
Where a platform admin comes from
Not from the interface. is_admin is not fillable, nothing grants it in a form or
a route, and a test asserts that only one file in the codebase sets it — so if
somebody adds a second way, that test asks whether they meant to.
php artisan felagi:admin ops@yourcompany.com
Shell access, in other words. Platform administration reaches across every workspace, and the boundary for it is the machine rather than a role somebody can be given from inside a customer's workspace.
Revoking the last one is refused: an installation with no platform admin cannot reach its own single sign-on or allowlist settings.
Two-factor authentication is not a role
It applies to everybody who signs in, regardless of role, and the exemption list is per address rather than per account or per workspace — deliberately, since anybody can create a workspace and a workspace-scoped exemption would be a way for any user to switch off a security control.
It lives in /system, on the platform-admin gate, for the same reason.
See Two-factor authentication.
Guarded attributes
is_admin and email_verified_at are not in User::$fillable, and a test enforces it. They must
be set explicitly with forceFill, so no registration or profile-update path can ever grant
platform administration through mass assignment.