# Guardrails: permissions, review, and cost
An agent that can send an email, merge a pull request, or move money is only as safe as the gates you put in front of those actions. The model is no longer just generating text. It is acting, and a confident hallucinationhallucinationA hallucination is when an AI model generates output that is fluent and confident but factually wrong, fabricated, or unsupported by its source data.Voir la définition complète → becomes a real-world consequence. This lesson is about the control layer: deciding what an agent may touch, when a human must approve, what should never run unsupervised, and how to stop a runaway loop from burning your budget.
Every guardrail you build maps to one of these:
1. Wrong action taken. The agent does something it shouldn't (deletes a record, emails the wrong client).
2. Right action, wrong scope. The agent does the correct thing but with too much reach (refunds the whole order instead of one line item).
3. Runaway cost. A loop, a retry storm, or a huge tool output that re-enters the context and spikes your tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.Voir la définition complète → bill.
Permissions address #1 and #2. Human review catches the high-stakes edge of both. Cost controls handle #3. Treat these as separate systems, not one big "be careful" instruction.
The single biggest mistake is putting safety in the prompt ("never delete anything without asking"). Prompts are suggestions. Permissions live in code and configuration, where the model cannot talk its way past them.
When you build with Custom GPTs and GPT Actions, you define exactly which APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.Voir la définition complète → endpoints the action can hit via the OpenAPI schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète →. The GPT cannot call an endpoint you didn't expose. Use OAuth in the action's auth config so each user acts with their own permissions, not a shared key. See the Actions documentation for the auth flow.
Connectors (Google Drive, SharePoint, GitHub, and others) inherit the connected account's permissions. If you connect a read-only service account, the agent gets read-only access. Provision the underlying account with least privilege before you connect it.
The ChatGPT agent browses, runs code, and uses connectors in a sandboxed environment, and it pauses to ask before consequential steps like submitting a form or making a purchase. Treat that built-in confirmation as the floor, not your whole strategy.
With the Responses API and Agents SDK, permissions are just code around your tool functions. The model proposes a call; your code decides whether to honor it.
ALLOWED_REFUND_CEILING = 100_00 # cents
def issue_refund(order_id: str, amount_cents: int) -> dict:
if amount_cents > ALLOWED_REFUND_CEILING:
return {
"status": "needs_review",
"reason": f"Amount {amount_cents} exceeds auto-approve ceiling",
}
result = payments.refund(order_id, amount_cents)
return {"status": "done", "refund_id": result.id}The model never sees the payment credentials and cannot raise the ceiling. The rule is enforced where it counts.
Here is the pattern the hook promised: an agent drafts an action, but a human approves it before it executes. This is a two-phase tool. Phase one returns a proposal. A human (or a separate approval step) confirms. Phase two commits.
Imagine a support agent that handles refund requests. You want it to do the tedious work (look up the order, read the policy, draft the refund) but never to actually move money on its own above a threshold.
from openai import OpenAI
client = OpenAI()
PENDING = {} # in production, use a real datastore
def propose_refund(order_id: str, amount_cents: int, reason: str) -> dict:
token = f"refund_{order_id}"
PENDING[token] = {"order_id": order_id, "amount_cents": amount_cents}
return {
"status": "awaiting_approval",
"approval_token": token,
"summary": f"Refund ${amount_cents/100:.2f} on {order_id}: {reason}",
}
def commit_refund(approval_token: str) -> dict:
job = PENDING.pop(approval_token, None)
if not job:
return {"status": "error", "reason": "unknown or already-used token"}
receipt = payments.refund(job["order_id"], job["amount_cents"])
return {"status": "done", "refund_id": receipt.id}The agent can call propose_refund freely. It physically cannot call commit_refund without a valid tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.Voir la définition complète →, and that tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.Voir la définition complète → only appears in your system after a human clicks approve. The model's job ends at "here is what I want to do." A person owns the "yes."
This is the mental model for every high-stakes automation: propose by AI, commit by human, with the commit step gated behind a tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.Voir la définition complète → the model can't forge.
Not every action deserves the same gate. Sort your tools into three buckets:
Make these buckets explicit in your design doc. "Which bucket is this tool in?" is the first question for any new capability you add.
A few categories belong in the third bucket no matter how good your agent is:
The test: *if the agent gets this wrong once, can I undo it before anyone is harmed?* If the answer is no, a human commits it. Scheduled tasks make this sharper, because a scheduled task in ChatGPT runs while you're not watching. Anything on a schedule should produce a draft or a notification, not fire an irreversible action into the void.
Vérification des acquis
1. According to the lesson, which failure mode does 'right action, wrong scope' describe?
2. Why does the lesson argue that putting safety instructions in the prompt (e.g., 'never delete anything without asking') is the biggest mistake?
3. In a Custom GPT using GPT Actions, what determines which API endpoints the action is able to call?
4. Select ALL correct answers about how permissions and authentication should be handled for agents per the lesson.
Sélectionnez toutes les réponses correctes.
5. Select ALL correct answers describing the three failure modes the lesson's guardrails are designed to prevent.
Sélectionnez toutes les réponses correctes.
Agents loop. That is their power and their financial risk. A single misbehaving run can call a tool fifty times, re-feed each bloated result into the context, and quietly run up a bill far larger than a normal chat. Cost control is a guardrail, not an afterthought.
The most important limit is the maximum number of steps an agent may take. In the Agents SDK, you set a turn limit so a stuck agent stops instead of spinning forever.
from agents import Agent, Runner
agent = Agent(name="support", instructions=SYSTEM_PROMPT, tools=[propose_refund])
result = Runner.run_sync(agent, "Refund order 8842, item was defective", max_turns=8)If the agent can't finish in eight turns, it halts. That single parameter is the difference between a bounded task and a runaway loop.
TokenTokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.Voir la définition complète → cost compounds because every tool result gets added to the context and re-sent on the next call. A 20,000-tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.Voir la définition complète → APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.Voir la définition complète → dump that the model only needed one field from will cost you on every subsequent turn. Truncate and summarize tool outputs before returning them.
def search_logs(query: str) -> dict:
rows = db.search(query, limit=2000)
return {"matched": len(rows), "top": rows[:5]} # not all 2000Return the count and a sample, not the whole haystack. The model rarely needs the raw dump, and your wallet definitely doesn't.
You don't need your most capable model for every step. Routing classification and extraction to a smaller, cheaper model and reserving the frontier model for genuine reasoning is one of the highest-leverage cost moves available. Check the current lineup and per-tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.Voir la définition complète → rates on the OpenAI pricing page, since the exact models and numbers shift over time.
Two more levers:
Set a hard monthly usage limit in your OpenAI account billing settings so a bug can't drain the budget. Add per-environment APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.Voir la définition complète → keys (one for dev, one for prod) so you can attribute spend and revoke the noisy one. Then watch the usage dashboard. A cost spike on a graph is an early warning; a surprise invoice is a postmortem.
A well-guarded agent has all three systems working at once:
1. Permissions decide what tools exist and what scope each one has, enforced in code.
2. Review gates sit in front of the consequential tools, with a propose-then-commit split so a human owns every irreversible "yes."
3. Cost controls cap the loop, trim what re-enters the context, route to the right model, and alert on spend.
None of these lives in the prompt. The prompt shapes behavior; the guardrails enforce limits. When you add a new capability, ask all three questions in order: *What can this touch? Who approves it? What does it cost if it loops?*
max_turns (or its equivalent), and truncate tool outputs so bloated results don't re-enter the context on every loop.