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" } }
RequirementValues
teams, groups, owns1–50 strings, ≤128 chars
elevationjira, servicenow (pagerduty is accepted but has no source yet)
modesbuild, 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).

  • allow rules take part only when satisfied.
  • deny and approval rules 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).