Skip to main content

Playbook: Customer-Support Copilot

The scenario

A copilot helps support agents resolve tickets. It can look up a customer, draft replies, and — the part that matters — issue refunds and update account settings. Those last actions change money and state, so they need bounded, verifiable authority.

The risk

Without a mandate, the copilot's authority is "whatever the support API key can do." A prompt-injected ticket, a confused plan, or a bug could issue a large refund, touch the wrong customer, or change a setting no one intended. Nothing would explain, after the fact, why the action was allowed.

The mandate design

FieldValueWhy
Principaluser:support-lead@acme.comA human owns the delegation
Delegateagent:support-copilotThe specific agent, not a vague class
Actionsbilling.refund.issue, account.setting.updateOnly the two write actions it needs
Resourceacme:customer:*Any customer, but nothing outside the customer domain
Constraintsrefunds require human review; 30-day expiry; rate limitForce a human in the loop and bound blast radius

Create the mandate

const mandate = await mandaitor.createMandate({
principal: {
type: "NATURAL_PERSON",
subject_id: "user:support-lead@acme.com",
display_name: "Support Lead",
},
delegate: {
type: "AGENT",
subject_id: "agent:support-copilot",
display_name: "Support Copilot",
},
scope: {
actions: ["billing.refund.issue", "account.setting.update"],
resources: ["acme:customer:*"],
effect: "ALLOW",
},
constraints: {
time: { duration: "P30D" },
rate_limits: { max_operations: 200, window_seconds: 3600 },
context: { requires_human_review: true },
},
});

Enforce it at runtime

Verify inside the tool handler the model calls — before the refund actually happens.

async function issueRefundTool(args: { customerId: string; amount: number; currency: string }) {
const result = await mandaitor.verify({
delegate_subject_id: "agent:support-copilot",
action: "billing.refund.issue",
resource: `acme:customer:${args.customerId}`,
context: { amount: args.amount, currency: args.currency, channel: "support" },
});

if (result.decision !== "ALLOW") {
// REVIEW or DENY — hand off to a human, do not issue the refund.
return { authorized: false, decision: result.decision, reason_codes: result.reason_codes };
}

const refund = await billing.issueRefund(args);
return { authorized: true, refund, proof: result.proof };
}
Turning a threshold into a decision

Mandaitor returns the decision; your handler decides how strict to be. A common pattern: treat any refund over a threshold as requiring review by checking result.decision === "REVIEW" (driven by the requires_human_review constraint) and routing it to a human queue.

What should be denied

Paste these into the Sandbox with the mandate above and confirm the agent cannot:

TestExpectedReason
billing.refund.issue on acme:customer:c_123REVIEWHuman review required by constraint
billing.subscription.cancel on acme:customer:c_123DENYAction not in scope
billing.refund.issue on acme:vendor:v_9DENYResource outside acme:customer:*
Any action after the mandate is revokedDENYMandate not active

If those four behave as expected, the copilot is bounded.

Evidence & review

Every ALLOW returns a signed proof — store it with the ticket. In the compliance dashboard, a reviewer can see which refunds the copilot was authorized to make and which were stopped, without reading agent logs.

Adapt it

  • Tighter scope: replace acme:customer:* with the specific customer on the open ticket (acme:customer:${ticketCustomerId}) so the copilot is bound to the case it is working.
  • No writes at all: drop the actions to read-only and let humans execute — see the data-analyst playbook.
  • Tiered approval: for higher-risk account changes, use the deployment playbook pattern.