Human-in-the-Loop for DevOps AI Agents
Give an AI agent kubectl or terraform access and one hallucinated flag can take down a namespace — this is the pattern for gating infra changes behind a human decision without slowing down routine ops.
Why DevOps agents need a different gate than chat agents
A support agent that sends a bad email is embarrassing. A DevOps agent that runs kubectl scale deployment api --replicas=0 or applies a Terraform plan with a dropped resource is an outage. The blast radius is bigger and the actions are frequently not reversible in the way a draft email is — you can't un-delete a database with a retry.
That changes what the approval gate needs to carry. It's not enough to show a human the command; they need to know whether re-running it is safe if something goes wrong mid-execution, and how to undo it if it turns out to be the wrong call. That's what Impri's idempotent and undo fields are for — they're optional, but for infra actions they're the difference between a reviewer approving blind and approving informed.
Gating a Terraform apply (TypeScript)
import { ImpriClient, ImpriRejected, ImpriExpired, ImpriTimeout } from "@impri/sdk";
const client = new ImpriClient({ apiKey: process.env.IMPRI_API_KEY! });
async function proposeApply(plan: TerraformPlan) {
const created = await client.createAction({
kind: "terraform.apply",
title: `Apply: ${plan.summary}`,
preview: { format: "markdown", body: plan.humanReadableDiff },
targetUrl: plan.ciRunUrl,
expiresIn: 3600, // infra plans go stale fast — 1 hour
idempotent: false, // re-applying a plan with a destroy step is not safe to blindly retry
undo: "terraform apply -target=<resource> using the previous state file in s3://tfstate-backups/",
});
try {
const action = await client.awaitDecision(created.id, { timeoutS: 1800 });
// action.status === 'approved' here — awaitDecision throws otherwise
const result = await runTerraformApply(plan); // the only place the apply actually runs
await client.reportResult(created.id, result.ok ? "executed" : "execute_failed", {
changed_resources: result.changedResources,
run_url: result.runUrl,
});
} catch (e) {
if (e instanceof ImpriRejected) {
console.log("Apply rejected — not running.");
} else if (e instanceof ImpriExpired) {
console.log("Approval window closed before a decision was made.");
} else if (e instanceof ImpriTimeout) {
console.log("Still pending server-side — check the inbox later.");
} else {
throw e;
}
}
}result.payload is worth using here specifically — a DevOps reviewer checking the inbox later wants to see which resources actually changed, not just "executed."
Scoping the blast radius before the gate
The gate is only as strong as what the agent can do without it. An agent that holds a kubeconfig with cluster-admin and calls kubectl directly can route around any wrapper you build. The pattern that holds up:
- The agent never gets direct cluster/cloud credentials.
- It calls an internal executor function — the one shown as
runTerraformApplyabove — that itself owns the credentials. - That executor refuses to run unless it received an
approveddecision object from Impri, not just a boolean flag the agent could fabricate in its own output.
That third point matters more for DevOps agents than most other use cases, because the cost of a bypassed gate is higher. See the integrations doc for the wrapper pattern in more depth.
Routing by severity
Not every infra action deserves the same friction. A useful split:
| Action | Review path |
|---|---|
| Scale replicas up/down within a bounded range | Auto-approve via a rule, skip human review |
Terraform apply touching prod, kubectl delete, IAM changes |
Route to Slack with idempotent: false and a filled undo field |
| Anything touching a datastore's data (not just infra) | Route to a second reviewer via audit log — keep the trail, since this is the case someone will ask about later |
Rules let low-stakes scaling actions skip the inbox entirely so the human queue stays for decisions that actually need a human.
What Impri does not do here
It does not validate the Terraform plan or the kubectl command for correctness — it shows the diff to a human and trusts their judgment, the same as it does for any other action kind. It is not a policy engine; if you need "block any apply that touches IAM regardless of who approves," that's an OPA/Sentinel policy gate upstream of Impri, not something Impri enforces itself. And it doesn't watch your infrastructure for drift — for that you'd run a watcher that polls state and opens an action when something changes outside the pipeline.
Next: the MCP doc if your agent runs inside Claude Code or another MCP client and you'd rather not write the SDK calls directly.