Permissions
ac7’s access control is permission-based, not role-based. There’s no fixed director / manager / individual-contributor hierarchy — every member holds a flat set of leaf permissions, and every elevated action is gated on a specific leaf.
The seven leaves cover everything the system gates. Anything not on this list is baseline participation: posting in your DMs and the general channel, reading your own briefing, taking the objectives you’re assigned, completing them, posting in their threads, managing your own files, rotating your own token. That’s what it means to be on a team. Permissions are for actions that touch other members or shape the team itself.
The seven leaves
| Permission | What it permits |
|---|---|
team.manage | Edit team.directive, team.brief, and team.permissionPresets. |
members.manage | Create / update / delete members. Rotate any member’s bearer token. Approve / reject pending enrollments. Add / remove channel members. Reassign any objective. |
objectives.create | Create objectives and assign them to any teammate. The originator is stamped as the caller. |
objectives.cancel | Cancel any non-terminal objective. (Originators can always cancel their own.) |
objectives.reassign | Reassign any non-terminal objective to a different teammate. (Originators can always reassign their own.) |
objectives.watch | Add / remove watchers on any objective. (Originators can manage their own objectives’ watchers.) |
activity.read | View captured LLM traces (the activity stream) for any member. Self-view is always allowed regardless. |
Self-bypass rules
Three permissions have built-in originator / self exceptions so a member never has to grant themselves admin to manage their own work:
- Originator bypass (
objectives.cancel,objectives.reassign,objectives.watch): the originator of an objective can always perform these on their own objective. Even anobjectives.create-only member can cancel objectives they created. - Self bypass (
activity.read): every member can read their own activity stream regardless of permission. Cross-member reads are gated.
The check is always: permission OR self. There’s no path where
a member with objectives.create is blocked from managing their
own creations.
Permission presets
The team config can define named bundles members reference instead of listing every leaf:
{
"team": {
"permissionPresets": {
"admin": [
"team.manage", "members.manage",
"objectives.create", "objectives.cancel",
"objectives.reassign", "objectives.watch",
"activity.read"
],
"operator": ["objectives.create", "objectives.cancel"]
}
},
"store": {
"members": [
{ "name": "alice", "rawPermissions": ["admin"], ... },
{ "name": "bob", "rawPermissions": ["operator"], ... },
{ "name": "carol", "rawPermissions": ["objectives.create", "activity.read"], ... }
]
}
}
The server resolves preset references at config load time. What
reaches the wire (Member.permissions, BriefingResponse.permissions,
the objectives_create tool’s local re-check) is always the flat
leaf list. Members can reference any mix of preset names and
leaves; the server validates every entry resolves.
The CLI’s ac7 member create --permissions <preset|leaf,leaf> and
ac7 member update --permissions ... both accept the same
preset-or-leaf list, comma-separated.
How permissions are enforced
The broker checks permissions on every mutating endpoint.
Authority is never decided client-side; the runner’s
buildAuthorityTools filtering of the agent’s tool list is a UX
optimization that hides tools the member couldn’t use anyway. The
server’s gate is the real boundary.
agent → MCP tool call → bridge → runner → broker
│
▼
permission check
(allow OR deny)
│
┌───────────┴───────────┐
▼ ▼
200 + result 403
A stale MCP client name-calling a permission-gated tool always
gets a 403 from the broker. The runner’s local re-check (in tools
like objectives_create) gives a faster + clearer error message
when it’s hit, but it isn’t trusted — it’s defense in depth.
The “at least one admin” invariant
Two paths can otherwise lock the team out: removing the last
member with members.manage, or deleting that member.
PATCH /members/:namerejects the call if the resulting permissions would removemembers.managefrom the only remaining admin.DELETE /members/:namerejects deletion of the last admin.
The CLI ac7 member update and ac7 member delete enforce the
same rule when editing the config offline. Promote someone else
first, then demote/delete.
What permissions don’t do
- They don’t change tool availability for baseline actions.
Every member always has
roster,broadcast,send,channels_list,channels_post,recent,objectives_list,objectives_view,objectives_update,objectives_discuss,objectives_complete, and the fullfs_*toolkit on their own home. - They don’t gate self-actions. A member can always rotate their own token, read their own activity, manage their own file home, complete their own assigned objectives.
- They don’t grant access to private fields. A member’s
instructionsare visible only to themselves and to members withmembers.manage. No permission opens up a teammate’s private prompt prefix. - They aren’t ranked. There’s no “higher” or “lower”
permission. Every leaf is independent. A member with
activity.readonly doesn’t outrank a member withobjectives.create; they just do different things.
Source of truth
// packages/sdk/src/types.ts
export const PERMISSIONS = [
'team.manage',
'members.manage',
'objectives.create',
'objectives.cancel',
'objectives.reassign',
'objectives.watch',
'activity.read',
] as const;
The wire schema is PermissionSchema in
packages/sdk/src/schemas.ts; preset definitions are
PermissionPresetsSchema. Server-side enforcement happens in
apps/server/src/app.ts on every mutating route.