Entitlements
Gate rules on who the developer is (directory groups, GitHub teams, Okta, Entra) and what they're working on right now (Jira, ServiceNow).
Requiring entitlements
A rule with requires matches only when every listed requirement is satisfied (any one value within each list):
{ "id": "team-write", "action": "git.push", "repository": "acme/payments-api",
"ref": "refs/heads/agent/*", "effect": "allow",
"requires": { "teams": ["payments"], "modes": ["build"] } }
{ "id": "hotfix", "action": "git.push", "repository": "acme/payments-api",
"ref": "refs/heads/main", "effect": "approval", "approvals": 2,
"requires": { "elevation": "jira" } }| Requirement | Values |
|---|---|
teams, groups, owns | 1–50 strings, ≤128 chars |
elevation | jira, servicenow (pagerduty is accepted but has no source yet) |
modes | build, operate, readonly, from the runtime assertion |
Fail-safe evaluation
Each requirement is satisfied, unsatisfied, or unknown (a source failed, or the identity input is missing, such as a teams requirement with no ghLogin).
allowrules take part only when satisfied.denyandapprovalrules take part unless unsatisfied.
An outage can therefore never lift a restriction or let a broader allow through. A requires.elevation of pagerduty has no source: the broker logs entitlements.elevation_without_source at load, and such rules never apply.
Sources
"entitlements": {
"static": { "path": "/etc/agentgate/directory.json" },
"githubTeams": { "org": "acme", "installationId": 1, "teams": ["payments"] },
"jira": { "baseUrl": "https://acme.atlassian.net", "tokenFile": "/run/secrets/jira_token", "allowedStatuses": ["In Progress"] },
"servicenow": { "baseUrl": "https://acme.service-now.com", "credentialFile": "/run/secrets/servicenow", "auth": "basic", "table": "change_request", "allowedStates": ["Implement"] },
"okta": { "baseUrl": "https://acme.okta.com", "tokenFile": "/run/secrets/okta_token", "groups": ["eng", "payments-oncall"] },
"entra": { "tenantId": "acme.onmicrosoft.com", "clientId": "00000000-0000-0000-0000-000000000000", "clientSecretFile": "/run/secrets/entra_client_secret", "groups": ["Engineering"] }
}static
A JSON export (for example from AD/Entra or Backstage, written by a scheduled job), read on each resolution. The human is matched case-insensitively.
{ "humans": { "dev@example.com": { "groups": [], "teams": [], "owns": [] } } }githubTeams
Checks GET /orgs/{org}/teams/{slug}/memberships/{ghLogin} for each listed slug with an org-scoped installation token (Members: read, no repositories) and adds slugs whose membership is active. ghLogin comes only from a verified runtime assertion.
okta
Looks up GET /api/v1/users/{email}/groups with an SSWS token that has only okta.users.read and okta.groups.read, and supplies the configured groups the human belongs to. An unknown user holds none; an API failure makes groups unknown.
entra
Microsoft Graph with client credentials (GroupMember.Read.All application permission): GET /users/{email}/memberOf/microsoft.graph.group, matched by display name against the configured groups. The access token is cached until shortly before expiry and refreshed on 401. Live Entra membership needs no scheduled export; keep static for Backstage ownership or other catalogues.
jira
For tasks like jira:PAY-12, reads the issue's status and assignee over https (no redirects, 10 s timeout) and sets elevation when the status is in allowedStatuses (default In Progress) and the assignee's email equals the runtime's human. tokenFile must be owner-only.
servicenow
For tasks like servicenow:CHG0030001 (or INC…, any 2–6 letter prefix and 5–12 digits), reads the record from the Table API (table defaults to change_request; use incident for incidents) and grants elevation: "servicenow" only when the record's display state is in allowedStates (default ["Implement"]) and assigned_to.email equals the runtime's human. auth is basic (credential file holds user:password) or bearer. A record for a different number never counts.
Caching
Envelopes are cached per assertion jti (static mode: per runtimeId), up to 10,000 entries. Positive results live 5 minutes, so removing someone from a group or closing a ticket takes effect within that time (or immediately on SIGHUP); results with a failed source, 30 seconds. Failures are logged with the source name only and counted in agentgate_entitlement_source_errors_total{source}; tokens and response bodies are never logged. Audit records and approvals carry the envelope summary so reviewers see why a rule matched.
The same sources bound what a developer may delegate at issuance (Issuing identities) and, with identity.delegation, what the broker accepts at verification (Runtime identity).