Authority & autonomy (the leash)¶
An agent is defined less by what it can do than by when it may do it alone. Telbox encodes that as a per-capability authority level plus per-capability limits. At runtime the resolver turns (capability, level, limits, action) into one of four outcomes — refuse, draft, ask, or auto-act — so every write a granted agent attempts is checked against the leash the owner set. This page documents that gradient honestly, including what auto-execution does not protect against.
The autonomy gradient¶
Each capability an agent touches (calendar, email, thread_replies, reminders, tasks, mute, purchases, …) carries one level. The level is the coarse leash; limits tighten it.
| Level | Decision | What the agent does |
|---|---|---|
disabled |
REFUSE | The capability is off-limits — the agent can't use it at all. |
draft_only |
DRAFT | Prepares the action; the owner finishes / sends it. |
ask_before_action |
ASK | Proposes the action; one confirm executes it (human-in-the-loop). |
auto_act_limited |
AUTO (within limits) | Does it now, within the stated limits, then logs it and offers undo. |
Unset capabilities are safe by default
An agent autos nothing until the owner explicitly grants auto_act_limited for a specific capability. A capability with no grant resolves to ASK, never auto. (The visual builder and SDK surface draft_only as the convenience default when you add a capability; the resolver's default for a wholly ungranted capability is ASK — both are the safe end of the gradient.)
An AUTO decision carries an undo window (undo_window_s, 45s by default, env-tunable via TELBOX_AGENT_UNDO_WINDOW_S) — the frictionless pattern is act + undo, not confirm first. Every other decision has a zero undo window because nothing irreversible happened yet.
Per-capability limits¶
auto_act_limited only auto-acts when the concrete action falls inside the capability's limits. These are the limit keys the resolver actually honors:
| Capability | Limit key | Checked against | Auto-acts when |
|---|---|---|---|
calendar |
max_duration_min |
the event's duration_min |
duration ≤ max |
calendar |
known_contacts_only |
the event's invitees_known |
all invitees are known contacts |
thread_replies |
max_chars |
the reply's char_count |
reply length ≤ max |
email |
approved_domains |
the message's recipient_domains |
every recipient domain is in the list |
purchases |
max_amount_cents |
the order's amount_cents |
amount ≤ max |
Over-limit never silently becomes AUTO — it falls back to ASK
Bounds are hard. A calendar event longer than max_duration_min, a reply over max_chars, an email to an unapproved domain, or a purchase over max_amount_cents does not quietly proceed — it escalates to ASK so a human confirms. The fallback reason is surfaced (e.g. calendar_over_limit:duration_exceeds_max).
High-risk capabilities require an explicit, satisfied limit
purchases and email are treated as high-risk (money / outbound mail are external and hard to reverse). They never auto-act on an empty limit set — email with no approved_domains and purchases with no max_amount_cents both fall back to ASK even at auto_act_limited. You must set a real bound for them to ever auto.
External & irreversible tools never auto-act¶
Above the authority resolver sits a harder gate. A tool whose side-effect class is external is removed from auto-execution before the resolver is even consulted — so it can never auto-act, regardless of grant or limits.
The clearest example is request_uber_ride (side_effects: "external"): even an auto_act_limited grant on its capability cannot make it fire on its own. It always produces a confirm step. Email-send-equivalents are likewise hard-gated. The rule: if the result reaches outside Telbox or can't be cleanly undone, a human confirms.
Two independent backstops
Auto-execution survives only when both are true: (1) the tool is not an external/irreversible side effect, and (2) the owner granted auto_act_limited and the action is within limits. Either one failing → no auto.
Injection posture (honest)¶
This is the part most platforms gloss. We don't.
A message_arrival trigger fires on content the agent did not author and cannot fully trust. With the gradient wired into the triggered runtime, a tool the owner granted auto_act_limited will auto-execute when its trigger fires — which means a reversible granted tool can act on instructions embedded in a message it reads. "A VIP-watcher agent that turns an incoming message into a task" is exactly this behavior, by design.
There is no taint/provenance gate that blocks auto-acting on freshly-read untrusted content for reversible tools — a blanket one would neuter every message-triggered auto-agent. The agent's system prompt does carry a standing instruction to treat message text, transcripts, and tool results as untrusted data, never instructions, but that is prompt-level steering, not a hard gate. The real, hard backstops are the two below — and the blast radius is deliberately bounded:
- External / irreversible tools never auto-execute — injected content or not.
request_uber_ride, email sends, anythingexternalis hard-gated out of auto. - Default is
draft_only— nothing autos until an explicitauto_act_limitedgrant for a specific capability. - Reversible + own-workspace only — the tools that can auto-fire (
create_task,create_reminder,create_calendar_event,mute_thread) are undoable and scoped to the grantor's own data. To inject at all, an attacker must already be a member of a thread the agent watches. - Bounded volume — one trigger ⇒ one deduped run ⇒ a per-run tool cap; the reply path additionally carries a per-agent send budget.
Adding a new irreversible auto-capability MUST come with a real taint gate
The current "reversible content can auto-act" contract is only safe because the actions are undoable and own-workspace. Do not widen auto-execution to a new irreversible capability without adding a real provenance/taint gate first.
Setting the leash in the IR¶
Authority lives in the agent's IR as a per-capability guard. Both SDKs expose ir.guard(level, ...limits); the visual builder (Studio) renders the same guards on each write node. The example below grants reminders full auto (reversible, low-risk), bounds thread_replies to short auto-replies, requires confirmation for calendar, and leaves email at draft-only — then previews exactly what would auto-act with dry_run (no LLM, no side effects) before publishing.
# Preview what this IR would do — no LLM, no side effects.
curl -X POST https://api.telbox.ai/v1/agents/dry-run \
-H "Authorization: Bearer tb_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Reply Nudge",
"persona": "Watch the thread; turn asks into reminders and short replies.",
"triggers": [{ "kind": "message_arrival" }],
"steps": [
{ "id": "s1", "type": "tool", "tool": "create_reminder",
"args": { "title": { "prompt": "follow up" } } },
{ "id": "s2", "type": "tool", "tool": "compose_email_draft",
"args": { "to": { "prompt": "the sender" } } }
],
"guards": { "capabilities": {
"reminders": { "level": "auto_act_limited" },
"thread_replies": { "level": "auto_act_limited", "limits": { "max_chars": 280 } },
"calendar": { "level": "ask_before_action" },
"email": { "level": "draft_only" }
} }
}'
# Then create it (same IR body):
# curl -X POST https://api.telbox.ai/v1/agents -H "Authorization: Bearer tb_live_…" -d '{ … }'
from telbox import TelboxClient, ir
tb = TelboxClient(api_key="tb_live_…")
agent = ir.agent(
"Reply Nudge",
persona="Watch the thread; turn asks into reminders and short replies.",
triggers=[ir.on_message()],
steps=[
ir.tool("s1", "create_reminder", title=ir.prompt("follow up")),
ir.tool("s2", "compose_email_draft", to=ir.prompt("the sender")),
],
guards={
# reversible + low-risk → auto-acts on a watched message
"reminders": ir.guard("auto_act_limited"),
# auto, but only short replies; longer ones fall back to ASK
"thread_replies": ir.guard("auto_act_limited", max_chars=280),
# one confirm before any calendar write
"calendar": ir.guard("ask_before_action"),
# high-risk: stays draft-only (and would need approved_domains to ever auto)
"email": ir.guard("draft_only"),
},
)
# Preview the leash before publishing — no LLM, no side effects.
preview = tb.dry_run(agent)
print(preview.effects)
created = tb.create_agent(agent)
import { TelboxClient, ir } from "@telbox/sdk";
const tb = new TelboxClient({ apiKey: "tb_live_…" });
const agent = ir.agent("Reply Nudge", {
persona: "Watch the thread; turn asks into reminders and short replies.",
triggers: [ir.onMessage()],
steps: [
ir.tool("s1", "create_reminder", { title: ir.prompt("follow up") }),
ir.tool("s2", "compose_email_draft", { to: ir.prompt("the sender") }),
],
guards: {
// reversible + low-risk → auto-acts on a watched message
reminders: ir.guard("auto_act_limited"),
// auto, but only short replies; longer ones fall back to ASK
thread_replies: ir.guard("auto_act_limited", { max_chars: 280 }),
// one confirm before any calendar write
calendar: ir.guard("ask_before_action"),
// high-risk: stays draft-only (and would need approved_domains to ever auto)
email: ir.guard("draft_only"),
},
});
// Preview the leash before publishing — no LLM, no side effects.
const preview = await tb.dryRun(agent);
console.log(preview.effects);
const created = await tb.createAgent(agent);
Same resolver, three surfaces
The dry-run preview, the interactive test-run, and the triggered worker all consult the same authority resolver — so what dry_run shows is what the live agent actually does. A preview that diverged from the runtime would be worse than no preview.
Where this fits¶
- The four levels are validated against the closed
authority_levelsvocabulary returned byGET /v1/agent-tools(alongside each tool'sside_effectsand thecapabilityits guard is keyed on). - Authority gates writes; it does not gate reads. A read-only tool has no governing capability.
- See AI & Privacy for the consent + quota gates that sit in front of any model call, and the confirm-card flow (
POST /v1/agent/confirm-action) that an ASK decision produces. - See Errors for the machine-readable codes a blocked or escalated action returns.