An AI agent needs the ability to do a job. It rarely needs the credential that unlocks every adjacent job.
That distinction is easy to blur. A support agent needs to answer one assigned question, so someone pastes a master login beside the prompt. The immediate task now works. The same login also lands in conversation history, copied task cards, debugging output, and any later context built from them. A narrow job has inherited broad authority.
AI Biz Lesson 8 gives the short version: keep the secret outside the prompt, grant the smallest permission for the shortest time, record the action, and revoke access. Here is a concrete version you can reproduce without connecting to a real account.
Start with the task contract
Use a synthetic support task. The agent may read thread demo-alpha and save one draft reply to that same thread. It may not list other threads, export data, change recipients, or send the message.
Write that contract as data before the agent starts:
task: answer-demo-alpha
resource: support-thread:demo-alpha
allow:
- read
- save_draft
expires_after_seconds: 600
maximum_actions: 2
on_scope_change: stop_for_human
This is not a nicer prompt. It is the input to the code that stands between the agent and the service. The agent can ask for broader access, but it cannot create that access by asking.
The useful test is mechanical: can the same capability read demo-beta? Can it send the saved draft? Can it act after ten minutes? Each answer should be no even when the request is perfectly formatted.
Put the secret behind the tool
The service credential belongs to a protected process. The agent receives a task-scoped handle, not the credential itself. The tool exchanges that handle for exactly the permitted operation and returns ordinary task data.
Here is a small Ruby simulation. It uses no network and no real secret, so you can save it as bounded_task.rb and run it with ruby bounded_task.rb:
Capability = Struct.new(
:task, :resource, :actions, :expires_at, :uses_left, :revoked,
keyword_init: true
)
class StopForHuman < StandardError; end
class SupportTool
attr_reader :receipts
def initialize(records, now: -> { Time.now })
@records = records
@now = now
@receipts = []
end
def perform(capability, action:, resource:, value: nil)
allowed = !capability.revoked &&
@now.call < capability.expires_at &&
capability.uses_left.positive? &&
capability.resource == resource &&
capability.actions.include?(action)
raise StopForHuman, "scope changed" unless allowed
result = case action
when :read
@records.fetch(resource).fetch(:question)
when :save_draft
@records.fetch(resource)[:draft] = value
"draft_saved"
else
raise StopForHuman, "unknown action"
end
capability.uses_left -= 1
@receipts << { task: capability.task, resource: resource, action: action }
result
end
end
records = {
"support-thread:demo-alpha" => { question: "How do I export my notes?" }
}
capability = Capability.new(
task: "answer-demo-alpha",
resource: "support-thread:demo-alpha",
actions: %i[read save_draft],
expires_at: Time.now + 600,
uses_left: 2,
revoked: false
)
tool = SupportTool.new(records)
question = tool.perform(capability, action: :read, resource: capability.resource)
tool.perform(
capability,
action: :save_draft,
resource: capability.resource,
value: "Open Notes, choose Export, then select a file format."
)
capability.revoked = true
puts question
puts tool.receipts
The example is deliberately small, but the boundary is real. The capability names one task, one resource, two allowed actions, a lifetime, and a use budget. The receipt contains what the verifier needs without containing a password.
Test the refusal path
A happy-path test proves that the tool works. It does not prove that the boundary works. Add three negative checks:
# A different thread must stop.
tool.perform(capability, action: :read, resource: "support-thread:demo-beta")
# Sending was never granted and must stop.
tool.perform(capability, action: :send, resource: capability.resource)
# Revoked access must stay revoked.
tool.perform(capability, action: :read, resource: capability.resource)
Run each check separately and expect StopForHuman. In production, the stop should create a reviewable event, not trigger an automatic retry with wider permissions. “Try again with more access” quietly converts a bounded capability back into a master credential.
The stop condition is part of the design
Stop before acting when any of these becomes true:
- The agent asks for a password, recovery value, or another raw secret.
- The resource identifier differs from the one in the task contract.
- The requested action is absent from the allowlist.
- The capability expired, reached its action budget, or was revoked.
- Completing the task now requires sending, exporting, purchasing, deleting, or changing an account.
A person can issue a new capability after reviewing the changed job. Do not silently widen the old one. A new grant makes the decision visible, preserves the old receipt, and gives the broader action its own expiry and limit.
This pattern does not make the agent trustworthy. It makes trust less relevant. The prompt describes the job; the capability bounds the authority; the receipt shows what happened; revocation ends the relationship.
That is enough structure for one useful task—and no password in the room.
If you want the physical reminder, the Microservices Overcoat Tee is the live shop destination paired with Lesson 8.