Modeling Agents as Principals
Most applications already have authorization models centered on human users. If you are building agents as part of your application, one common pattern is to model agent as a first-class principal - the same way users are granted access to resources today.
This is orthogonal to patterns like Task-Based Authorization. Even if you use task-scoped permissions, you still want to constrain what an agent can do in general - beyond the permissions required for a specific task.
This guide shows how to model agents as principals in a OpenFGA authorization model using an issue-tracking application as an example. The agent receives permissions on domain resources (projects, issues) and inherits access through the same hierarchy that users do.
Authorization model
The following issue-tracking model treats agent as a first-class principal alongside user:
model
schema 1.1
type user
type agent
type organization
relations
define admin: [user]
define member: [user, agent]
type project
relations
define organization: [organization]
define owner: [user]
define member: [user, agent]
define can_delete: owner or admin from organization
define can_edit: owner or admin from organization
define can_read: can_edit or member or member from organization
define can_create_issue: can_edit
type issue
relations
define project: [project]
define reporter: [user, agent]
define assignee: [user, agent]
define can_delete: reporter or can_delete from project
define can_edit: assignee or can_edit from project
define can_read: reporter or can_edit or can_read from project
If your current model is user-centric, the main change is to allow agent anywhere you already allow user for the relationships an agent should hold.
In this example:
agentis added as an allowed subject onorganization.member,project.member,issue.reporter, andissue.assignee.- No new permission types or hierarchies are needed - agents participate in the same structure as users.
This example keeps the original permission semantics intact. The only change is that agent can now appear in the same relations as user. If your application wants project members to edit projects or issues, that is a separate permission-model decision.
Granting access to agents
Grant an agent membership on a specific project:
tuples:
- user: agent:triage-bot
relation: member
object: project:alpha
Because can_read includes member, this single tuple lets agent:triage-bot read the project and its issues. The agent still cannot edit or delete anything - can_edit and can_delete retain the original owner and admin requirements.
Checking permissions
The examples below assume the issue is linked to its project:
tuples:
- user: project:alpha
relation: project
object: issue:issue-123
Check whether the agent can read a specific issue. The agent's project membership grants access:
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
- Pseudocode
- Playground
// Run a check
const { allowed } = await fgaClient.check({
user: 'agent:triage-bot',
relation: 'can_read',
object: 'issue:issue-123',
}, {
authorizationModelId: '01HVMMBCMGZNT3SED4Z17ECXCA',
});
// allowed = true
options := ClientCheckOptions{
AuthorizationModelId: openfga.PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientCheckRequest{
User: "agent:triage-bot",
Relation: "can_read",
Object: "issue:issue-123",
}
data, err := fgaClient.Check(context.Background()).
Body(body).
Options(options).
Execute()
// data = { allowed: true }
var options = new ClientCheckOptions {
AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA"
};
var body = new ClientCheckRequest {
User = "agent:triage-bot",
Relation = "can_read",
Object = "issue:issue-123",
};
var response = await fgaClient.Check(body, options);
// response.Allowed = true
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA",
}
body = ClientCheckRequest(
user="agent:triage-bot",
relation="can_read",
object="issue:issue-123",
)
response = await fga_client.check(body, options)
# response.allowed = true
var options = new ClientCheckOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientCheckRequest()
.user("agent:triage-bot")
.relation("can_read")
._object("issue:issue-123");
var response = fgaClient.check(body, options).get();
// response.getAllowed() = true
fga query check --store-id=$FGA_STORE_ID --model-id=01HVMMBCMGZNT3SED4Z17ECXCA agent:triage-bot can_read issue:issue-123
# Response: {"allowed":true}
curl -X POST $FGA_API_URL/stores/$FGA_STORE_ID/check \
-H "Authorization: Bearer $FGA_API_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA",
"tuple_key": {
"user": "agent:triage-bot",
"relation": "can_read",
"object": "issue:issue-123"
}
}'
# Response: {"allowed": true}
check(
user = "agent:triage-bot", // check if the user `agent:triage-bot`
relation = "can_read", // has an `can_read` relation
object = "issue:issue-123", // with the object `issue:issue-123`
authorization_id = "01HVMMBCMGZNT3SED4Z17ECXCA"
);
Reply: true
is agent:triage-bot related to issue:issue-123 as can_read?
# Response: A green path from the user to the object indicating that the response from the API is `{"allowed":true}`
The agent cannot delete the issue because can_delete requires reporter or project-level can_delete (which requires owner or admin):
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
- Pseudocode
- Playground
// Run a check
const { allowed } = await fgaClient.check({
user: 'agent:triage-bot',
relation: 'can_delete',
object: 'issue:issue-123',
}, {
authorizationModelId: '01HVMMBCMGZNT3SED4Z17ECXCA',
});
// allowed = false
options := ClientCheckOptions{
AuthorizationModelId: openfga.PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientCheckRequest{
User: "agent:triage-bot",
Relation: "can_delete",
Object: "issue:issue-123",
}
data, err := fgaClient.Check(context.Background()).
Body(body).
Options(options).
Execute()
// data = { allowed: false }
var options = new ClientCheckOptions {
AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA"
};
var body = new ClientCheckRequest {
User = "agent:triage-bot",
Relation = "can_delete",
Object = "issue:issue-123",
};
var response = await fgaClient.Check(body, options);
// response.Allowed = false
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA",
}
body = ClientCheckRequest(
user="agent:triage-bot",
relation="can_delete",
object="issue:issue-123",
)
response = await fga_client.check(body, options)
# response.allowed = false
var options = new ClientCheckOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientCheckRequest()
.user("agent:triage-bot")
.relation("can_delete")
._object("issue:issue-123");
var response = fgaClient.check(body, options).get();
// response.getAllowed() = false
fga query check --store-id=$FGA_STORE_ID --model-id=01HVMMBCMGZNT3SED4Z17ECXCA agent:triage-bot can_delete issue:issue-123
# Response: {"allowed":false}
curl -X POST $FGA_API_URL/stores/$FGA_STORE_ID/check \
-H "Authorization: Bearer $FGA_API_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA",
"tuple_key": {
"user": "agent:triage-bot",
"relation": "can_delete",
"object": "issue:issue-123"
}
}'
# Response: {"allowed": false}
check(
user = "agent:triage-bot", // check if the user `agent:triage-bot`
relation = "can_delete", // has an `can_delete` relation
object = "issue:issue-123", // with the object `issue:issue-123`
authorization_id = "01HVMMBCMGZNT3SED4Z17ECXCA"
);
Reply: false
is agent:triage-bot related to issue:issue-123 as can_delete?
# Response: A red object indicating that the response from the API is `{"allowed":false}`
Direct assignment for fine-grained control
Instead of granting project-wide access, you can assign the agent directly to a specific issue:
tuples:
- user: agent:triage-bot
relation: assignee
object: issue:issue-456
Now the agent can edit issue:issue-456 but has no access to other issues in the project.
Organization-level agent access
You can also grant an agent membership at the organization level. This gives it read access to all projects in the organization:
tuples:
- user: agent:reporting-bot
relation: member
object: organization:acme
The agent inherits can_read on every project that belongs to organization:acme, and through that, can read all issues in those projects.
When to use this pattern
Modeling agents as principals works well when:
- Your application already has a user-centric authorization model.
- Agents act inside your application's own domain.
- You want agents to inherit access through the same resource hierarchy as users.
- You want to grant durable, non-task-specific permissions such as project membership or issue assignment.
This pattern does not replace task-based authorization. It defines what an agent can do in general. Task-based authorization can then further constrain that access at runtime.
Migration strategy
If your application already has a user-centric model:
- Add the
agenttype to your model. - Include
agentin selected relations where agents need access (member,assignee,reporter, etc.). - Write tuples to grant agents access at the appropriate scope (organization, project, or issue).
- Check permissions using the same API calls you use for users - just with
agent:as the subject. If you want to check for both user and agent permissions, perform a batch check call.
Recommendations
- Least privilege: grant the narrowest scope possible. Prefer project-level membership over organization-level membership.
- Avoid wildcards: do not use
agent:*in production grants.
Related sections
Grant agents access to perform specific actions only when necessary, with task-scoped permissions.