HITL approval authority and role-based Rego
HITL approval authority and role-based Rego
Customers often ask whether Admin, DevOps, or User roles control who may approve agent actions in the Human-in-the-Loop (HITL) inbox. This guide separates three governance questions, shows what you should expect, and ships copy-paste Rego you can try in the OPA Playground.
Related: Writing Rego policies, Persona agents, workflows, and skills, bundled policies in modules/aios-policies/policies/.
Three different questions
| Question | Plain meaning | What the platform does |
|---|---|---|
| Who may click Approve? | When a pending card appears, which humans may resolve it (approve or reject)? | Optional Rego — attach an approval_authorization policy to the agent. If none is attached, any authenticated member of the same tenant can approve. |
| Who may invoke a tool? | Before HITL even triggers, may this caller run terraform_apply, prod shell, etc.? |
Logic Rego on input.user.projects[].role_name / input.current_project.role_name |
| When is HITL required? | Should this tool call pause for human sign-off? | Intervention Rego (approval_required) |
Do not conflate them. A logic policy can deny a developer before any approval card appears. An intervention policy can require HITL for a shell command even when the caller is allowed to request it. An approval_authorization policy decides who may click Approve on the card.
What to expect (happy path)
- The agent tries a sensitive tool (for example a Grafana or shell
*_execute_command). - An intervention policy sets
approval_required→ a pending approval appears in the HITL inbox (and Slack/Teams if connected). - Someone clicks Approve or Reject:
- If the agent has no
approval_authorizationpolicy → any authenticated user in that tenant may resolve (same as before). - If the agent has one or more
approval_authorizationpolicies → the platform evaluates Rego with the clicker’s identity asinput.approver.- Allowed → HTTP 200, the tool continues (or the rejection is recorded).
- Denied → HTTP 403 with a clear “not authorized” message; the card stays pending.
- If the agent has no
flowchart LR
subgraph toolCall [When the agent calls a tool]
regoLogic[Logic: may this caller invoke?]
regoHitl[Intervention: pause for HITL?]
end
pending[Pending approval card]
resolve[Human clicks Approve or Reject]
approverRego[approval_authorization Rego]
toolCall --> regoLogic
regoLogic -->|deny| block[Blocked — no card]
regoLogic -->|allow| regoHitl
regoHitl -->|approval_required| pending
regoHitl -->|no gate| execute[Tool runs]
pending --> resolve
resolve --> approverRego
approverRego -->|allow| execute
approverRego -->|deny| stayPending[Stays pending — 403]
Who may click Approve
When an operator resolves a pending approval (POST /api/v1/approvals/{id}), the platform always checks:
- Authentication — a valid StackGen principal is on the request.
- Tenant isolation — the approval belongs to the caller’s org.
Then, if the agent has approval_authorization policies attached:
- Approver Rego —
input.approver.roleis the clicker’s project role in the current workspace (for exampleadmin,developer). You can also gate oninput.approver.email, the tool name, and who requested the call (input.requester).
If those policies are not attached, step 3 is skipped (fail-open for approvers). That is intentional so existing agents keep working until you opt in.
Slack / Teams approval buttons still require the chat user to map to a StackGen member. After that membership check, the same approval_authorization rules apply.
Live check (dogfood): With Grafana tools behind HITL, an unauthorized resolve returned 403 and left the approval pending; an authorized admin resolve returned 200 and marked it approved. See Guild QA notes: HITL approver authorization.
Project roles on the factsheet
| Field | When it appears | Meaning |
|---|---|---|
input.user / input.current_project |
Tool-call (logic / intervention) | Caller identity and project role_name |
input.approver |
Approve / reject only | Clicker identity; role mirrors current-project role_name |
input.requester |
Approve / reject only | Who triggered the original tool call (from the approval record) |
These are per-project roles from appcd — not the same thing as org-wide “Workspace Admin” used for some API actions (for example workflow sharing).
If appcd identity cannot be resolved when approval_authorization policies are attached, resolve fails closed (403) — the platform will not silently let an unknown clicker through.
Sample caller factsheet: policy-evaluation-input-user-roles.json.
Policy types at a glance
| Policy type | Primary rules | What it controls |
|---|---|---|
| logic | allow, deny_reason |
Whether the caller may invoke the tool at all |
| intervention | approval_required, approval_reason |
Whether execution pauses for human sign-off |
| approval_authorization | allow, approval_reason on input.approver.* |
Whether the human may approve or reject the pending card |
Recommended layering:
- Logic — narrow who can even attempt sensitive tools.
- Intervention — require HITL for tools that should always pause.
- Approval authorization — restrict who may clear the pending card (admin-only, named emails, tool-specific approvers, …).
Attach all three to the same agent when you need the full chain. Intervention alone creates the card; approval_authorization alone does nothing until something is pending.
Pattern: gate tool invocation by role
Use a logic policy when only certain project roles may invoke a tool — for example only admin or devops may call terraform_apply.
Copy-paste starter: hitl-caller-role-gate.rego.
A developer calling terraform_apply is denied before HITL — no approval card is created.
opa test docs/samples/hitl-caller-role-gate.rego docs/samples/hitl-caller-role-gate_test.rego
Pattern: require HITL for sensitive tools
Use an intervention policy when the tool may run after human sign-off.
Copy-paste starter: hitl-intervention-shell.rego (covers run_shell and *_execute_command, including Grafana-style integration tools).
Upload with type = "intervention" via sg_policy.
opa test docs/samples/hitl-intervention-shell.rego docs/samples/hitl-intervention-shell_test.rego
Pattern: restrict who may Approve
Use an approval_authorization policy on the same agent that has the intervention policy.
Copy-paste starter: hitl-approver-admin-only.rego — only clickers whose current-project role is admin may resolve.
Bundled design reference (richer email / per-tool mapping): modules/aios-policies/policies/hitl-approval.rego — upload that (or a trimmed variant) as type = "approval_authorization", not as logic.
opa test docs/samples/hitl-approver-admin-only.rego docs/samples/hitl-approver-admin-only_test.rego
What you will see
| Situation | Expect |
|---|---|
Developer clicks Approve, policy requires admin |
403 — card remains pending; tool does not run |
| Admin clicks Approve | 200 — approval resolved; tool proceeds |
No approval_authorization on the agent |
Any tenant member can approve (opt-in feature) |
Dry-run in Policy Studio / POST /api/v1/policies/evaluate |
Set policy_type to approval_authorization and pass an approver block to preview allow vs deny |
Try it in the OPA Playground
Caller role gate (logic)
- Open play.openpolicyagent.org.
- Paste hitl-caller-role-gate.rego.
- Paste policy-evaluation-input-user-roles.json into Input.
- Evaluate
data.policy.allow— expectfalsefor a developer onterraform_apply; change role toadmin→true.
Intervention (shell / execute_command HITL)
- Replace the policy with hitl-intervention-shell.rego.
- Set
tool.nametorun_shellorhitl-grafana_execute_command. - Evaluate
data.policy.approval_required— expecttrue.
Approver gate (approval_authorization)
- Paste hitl-approver-admin-only.rego.
- Use input like:
{
"agent": { "id": "platform-sre-agent", "name": "platform-sre-agent" },
"timestamp": "2026-08-26T01:00:00Z",
"tool": { "name": "hitl-grafana_execute_command", "arguments": { "command": "echo ok" } },
"approver": { "id": "u1", "email": "dev@example.com", "role": "developer" },
"decision": "approve"
}
- Evaluate
data.policy.allow— expectfalse. Changeapprover.roletoadmin→true.
Practical tips (from live validation)
- Attach intervention and approval_authorization to the agent that actually calls the tool. If a planner agent creates a sub-agent, policies on the parent do not automatically protect the child’s tools — deny or avoid
create_agent, or attach policies on the executing agent. - Prefer a simple gated tool for a smoke test (for example
*_execute_commandwithecho …). Some Grafana tools need extra arguments (likerule_uid) before the model will call them. - Local-only stacks may always report
adminfor the clicker. Use Policy Studio dry-run with an explicitapprover.roleofdeveloperto preview deny, or test against a real appcd project with a non-admin member. - Do not put integration tokens in docs or git — store them in Vault / StackGen secrets.
Customer FAQ
Can a User approve an Admin’s pending shell command?
Only if you have not attached an approval_authorization policy — then any tenant member can approve. With an admin-only approval policy attached, a developer / User clicker gets 403 and the card stays pending.
Can we block Users from running prod terraform?
Yes. Attach a logic policy like hitl-caller-role-gate.rego. Developers are denied at tool-call time.
Does required_approvals: ["admin"] on a remediation pattern enforce Admin approvers?
It marks the pattern as requiring HITL when the list is non-empty. Use an approval_authorization policy to validate the resolver’s role at approve time.
Are Admin / DevOps / User the same as role_name in Rego?
Not necessarily. input.approver.role / input.current_project.role_name come from appcd project membership (admin, devops, developer, …). Org-level Admin/DevOps used for API features like workflow sharing are a different authorization layer.
Ship policies with Terraform
resource "sg_policy" "caller_role_gate" {
name = "caller-role-gate"
description = "Only admin/devops may invoke listed production tools"
type = "logic"
rego_source = file("${path.module}/policies/hitl-caller-role-gate.rego")
}
resource "sg_policy" "shell_hitl" {
name = "shell-hitl"
description = "Require human approval for shell and remote execution"
type = "intervention"
rego_source = file("${path.module}/policies/hitl-intervention-shell.rego")
}
resource "sg_policy" "approver_admin_only" {
name = "approver-admin-only"
description = "Only project admins may approve or reject pending HITL"
type = "approval_authorization"
rego_source = file("${path.module}/policies/hitl-approver-admin-only.rego")
}
resource "sg_agent_policy" "caller" {
agent_name = "platform-sre-agent"
policy_id = sg_policy.caller_role_gate.id
}
resource "sg_agent_policy" "hitl" {
agent_name = "platform-sre-agent"
policy_id = sg_policy.shell_hitl.id
}
resource "sg_agent_policy" "approvers" {
agent_name = "platform-sre-agent"
policy_id = sg_policy.approver_admin_only.id
}
See Writing Rego policies for CI checks (make opa-check, make opa-test).