Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Start here
Concepts
Release notes
What's new
Elyra

Permissions

Four 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;
});

// Whether somebody is here to do the work or to read about it.
Gate::define('work-in-workspace', function (User $user, ?Workspace $workspace = null) {
    $workspace ??= $user->currentWorkspace();

    return $user->roleIn($workspace)?->worksOnIssues() === true;
});

// Reading who is carrying what, and who is behind.
Gate::define('read-people', function (User $user, ?Workspace $workspace = null) {
    $workspace ??= $user->currentWorkspace();

    return $user->roleIn($workspace)?->canReadPeople() === 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 admin but not administer-workspace in a workspace they are not a member of.


Roles inside a workspace

Role Can
Management Read the Timeline and the reports. Nothing else, and not assigned work
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

Management is not a rung on that ladder. The other four are ordered — each does everything the one below it does — and this one is beside them. It reads.

Which is why it is not "Member, restricted". A director handed a board and an inbox starts appearing in the workload report as somebody carrying nothing, and the report is then wrong for everybody who reads it. So the role cannot be handed work at all: it is absent from every assignee picker, and assignTo() refuses it whichever door the request came through — the interface, the API or the daemon.

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
work-in-workspace everyone except management Every working surface: the board, epics, projects, the inbox, the Gantt, cycles, timesheets, meetings, knowledge, the whiteboard, the agent roster
read-people owner, admin, management Who is carrying what and who is behind: the standup report, and the Timeline grouped by person
write-knowledge everyone except management Knowledge base articles, whiteboards
write-issues, write-projects everyone except management Editing an issue's or a project's own words, and an epic's milestones

work-in-workspace is carried by a route group rather than by each page, so a page added to that group tomorrow inherits it without anybody remembering this file. The sidebar reads the same gate in one condition rather than eleven.

canManageAgents() was !== Member, which was correct for exactly as long as there were four roles. Adding a fifth would have handed it the entire administration section, because the new role was not the one the condition named. It is an allowlist now.

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 nearly pointless — the check that matters is that somebody does the work, and every role but Management 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 The Timeline, the reports, the manual, settings
/app (inner group) work-in-workspace Dashboard, projects, issues, epics, inbox, Gantt, cycles, timesheets, meetings, knowledge, whiteboard, agent roster
/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.


Who is behind on what

The Standup report — everybody's open work, grouped by who is carrying it, with overruns marked — is the one report a member cannot open. So is the Timeline grouped by person. Both sit behind read-people: owner, admin and management.

That gate names the question rather than standing in for it. It was administer-workspace until the management role arrived, which was the nearest existing gate and not the right one: reading who is behind is a kind of trust an owner, an admin and a director all have, and a developer deliberately does not. A developer hires agents, connects machines and writes skills without being handed it.

Three doors, and each has a reason:

Where Why
The route (can:administer-workspace) Where somebody auditing the application reads which routes are restricted
The report's own data A Livewire update does not re-run mount(), so this is what closes on somebody demoted while the page sat open
The CSV export A plain link with the filters in the query string, and the easier of the two doors to reach with a pasted URL

The report index does not draw a tile somebody cannot use. A card that answers 403 reads as a broken product rather than as a report that is not theirs.

And the Gantt cannot be talked into it. That page binds its grouping to the query string, so ?groupBy=assignee was one link away from the same data — as was the Gantt's own CSV export, which read the parameter straight out of the request. Both now go through one allowlist that names the two open groupings, so a third surface cannot reopen what the first two closed.

The Timeline's own group parameter is bound the same way and goes through the same shape of allowlist, built per reader: person is simply not in the list for somebody without read-people, so asking for it in the address returns the default.

What a role that only reads cannot reach

Management is defined by its doors, so they were counted rather than remembered.

Door What closes it
Every working page The work-in-workspace route group
Every link to one The same gate, once, in the sidebar
The dashboard It redirects to the Timeline rather than refusing. Eight places send somebody to dashboard after a login, an accepted invitation, a password change or a two-factor challenge, and a gate there would have made each of them a 403 for the person who just proved who they are
⌘K and g-then-a-letter Not rendered. The palette searches issues, projects, boards and notes — every one behind the gate — so it would be a box that finds what it cannot open
The Gantt's CSV A gate on its catalogue entry, which closes the card, the export and the page together
Attachments roleIn(...)->worksOnIssues(), on the file route. Every attachment hangs off an issue, a comment or an article; the route takes a parameter, so the route sweep never tried it
The API Refused in AuthenticateApi, with a 403 that says why. The one role that cannot open an issue could otherwise have read every issue in the workspace with a read token from its own settings page
Being handed work Workspace::workers() — the relation every assignee picker reads — and assignTo() behind it

A sweep tries every unparameterised /app route as a Management user and asserts the exact list that answers 200. It is written as an allowlist because the failure it catches is a new page added outside the group, and only a sweep finds the one nobody remembered.


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.

Account maintenance is not workspace administration

is_admin — the platform gate — is the only thing that may set somebody else's password or create an account outright. A workspace owner cannot, and the distinction is worth stating because it is easy to get backwards:

Whose
Remove somebody from this workspace The workspace owner's. It is workspace-scoped
Change their role or weekly hours The workspace owner's, same reason
Set a password on their account The platform administrator's. A person can be in three workspaces, and owning one says nothing about their account

Neither of them can clear another person's two-factor authentication. Nothing in Félagi can. If it could, a password reset would be an account takeover in two clicks. See Inviting your team.