From 30199414a9bb425ac3775adb5aecd4cf3197a40c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 12:53:29 +0000 Subject: [PATCH 1/4] Add action authority demo walkthrough blog post Follow-up blog post for the cycles-agent-action-authority-demo repo. Walks through the support agent scenario, shows unguarded vs guarded output, explains the three-decorator code change, and covers toolset scoping mechanics. SEO-optimized title and description for action control search queries. https://claude.ai/code/session_015zc6uasNhYkXQqTeTRFUMv --- ...uthority-demo-support-agent-walkthrough.md | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 blog/action-authority-demo-support-agent-walkthrough.md diff --git a/blog/action-authority-demo-support-agent-walkthrough.md b/blog/action-authority-demo-support-agent-walkthrough.md new file mode 100644 index 0000000..b248941 --- /dev/null +++ b/blog/action-authority-demo-support-agent-walkthrough.md @@ -0,0 +1,216 @@ +--- +title: "AI Agent Action Authority: Block Unauthorized Emails and Side Effects Before Execution" +date: 2026-03-22 +author: Cycles Team +tags: [action-authority, demo, agents, runtime-authority, walkthrough, action-control, side-effects] +description: "A support agent has access to CRM, notes, and email — but should every run be allowed to send? This demo walkthrough shows how Cycles blocks a customer email before execution while allowing internal actions to proceed. Three decorators, one exception, zero unauthorized side effects." +blog: true +sidebar: false +--- + +# AI Agent Action Authority: Block Unauthorized Emails and Side Effects Before Execution + +A support agent handles a billing dispute. Its workflow has four steps: read the case, log an internal note, update the CRM status, and send the customer a reply. Without a runtime decision layer, all four steps execute — including the email. With Cycles, the first three steps proceed normally. The fourth — `send_customer_email` — is blocked before execution because the `send-email` toolset has no provisioned budget. The email function never runs. The customer never receives an unauthorized message. + +This post walks through the [action authority demo](https://github.com/runcycles/cycles-agent-action-authority-demo) step by step: what the agent does, how the unguarded and guarded runs differ, and what the code change looks like. + + + +## The scenario + +Customer case #4782: Acme Corp's invoice shows $847, but their contract says $720. A support automation agent (`support-bot`) picks up the case and runs a four-step workflow: + +| Step | Tool | Toolset | Risk level | +|:----:|------|---------|------------| +| 1 | `read_case` | *(local)* | Read-only — no state change | +| 2 | `append_internal_note` | `internal-notes` | Write-local — internal log entry | +| 3 | `update_crm_status` | `crm-updates` | Mutation — changes case state | +| 4 | `send_customer_email` | `send-email` | Write-external — irreversible once delivered | + +Steps 1–3 are internal operations. Step 4 is the consequential one: once the email is sent, it cannot be unsent. In the [action-control taxonomy](/blog/ai-agent-action-control-hard-limits-side-effects), internal notes fall at tier 2 (write-local, reversible) and outbound email at tier 3 (write-external, irreversible). The risk difference is not about cost — all four actions cost the same in model terms. It is about what happens if the action should not have been taken. + +The tools in this demo are mocked. No real CRM, email service, or ticketing system is involved. The action authority is real. + +## Without Cycles: all actions execute + +When the agent runs without Cycles, every step completes with a green checkmark: + +``` +╭──────────── Support Case #4782 ─────────────╮ +│ Customer: Acme Corp (jane@acme.com) │ +│ Subject: Invoice shows $847, contract $720 │ +│ Agent: support-bot │ +│ Mode: UNGUARDED │ +╰──────────────────────────────────────────────╯ + +╭──────────── Action Log ──────────────────────╮ +│ ✓ read_case │ +│ ✓ append_internal_note [internal-notes] │ +│ ✓ update_crm_status [crm-updates] │ +│ ✓ send_customer_email [send-email] │ +╰──────────────────────────────────────────────╯ + +╭──────────── Result — UNGUARDED ──────────────╮ +│ All actions executed — including the email. │ +│ 4 actions approved · 0 actions blocked │ +╰──────────────────────────────────────────────╯ +``` + +The agent did exactly what it was told. That is the problem. No approval gate existed, so the email went out unchecked. In production, this means a customer receives a potentially premature or incorrect message — and you find out after the fact. + +## With Cycles: the email is blocked + +Same agent, same tools, same workflow. The only difference is that each tool call now passes through the Cycles server before execution. The first three steps still succeed. The fourth does not: + +``` +╭──────────── Support Case #4782 ─────────────╮ +│ Customer: Acme Corp (jane@acme.com) │ +│ Subject: Invoice shows $847, contract $720 │ +│ Agent: support-bot │ +│ Mode: GUARDED │ +╰──────────────────────────────────────────────╯ + +╭──────────── Action Log ──────────────────────╮ +│ ✓ read_case │ +│ Loaded case #4782 — Acme Corp │ +│ │ +│ ✓ append_internal_note [internal-notes] │ +│ POST /v1/reservations → 200 ALLOW │ +│ Billing discrepancy: $847 vs $720 │ +│ │ +│ ✓ update_crm_status [crm-updates] │ +│ POST /v1/reservations → 200 ALLOW │ +│ Status: Open → Investigating │ +│ │ +│ ✗ send_customer_email [send-email] │ +│ POST /v1/reservations → 409 BUDGET_EXCEEDED│ +│ Email NOT sent — escalated to human. │ +╰──────────────────────────────────────────────╯ + +╭──────────── Result — GUARDED ────────────────╮ +│ Cycles blocked the customer email before it │ +│ was sent. │ +│ 3 actions approved · 1 action blocked │ +╰──────────────────────────────────────────────╯ +``` + +The `send_customer_email` function never executed. Not "rolled back." Not "logged and flagged for review." The function body never ran. The Cycles server returned `409 BUDGET_EXCEEDED` on the reservation attempt, the `@cycles` decorator raised `BudgetExceededError`, and the agent caught the exception and reported: *"Email NOT sent — escalated to human for approval."* + +## The code change + +The diff between `unguarded.py` and `guarded.py` is exactly this: + +```python +# --- Import the SDK --- +from runcycles import ( + BudgetExceededError, CyclesClient, CyclesConfig, + cycles, set_default_client, +) + +# --- Initialize the client --- +config = CyclesConfig( + base_url=os.environ["CYCLES_BASE_URL"], + api_key=os.environ["CYCLES_API_KEY"], + tenant=os.environ["CYCLES_TENANT"], + agent="support-bot", +) +set_default_client(CyclesClient(config)) + +# --- Three decorators with toolset scoping --- +@cycles(estimate=COST_PER_ACTION_MICROCENTS, action_kind="tool.notes", + action_name="append-note", toolset="internal-notes") +def append_internal_note(case_id, note): + return _append_note(case_id, note) + +@cycles(estimate=COST_PER_ACTION_MICROCENTS, action_kind="tool.crm", + action_name="update-status", toolset="crm-updates") +def update_crm_status(case_id, old_status, new_status): + return _update_status(case_id, old_status, new_status) + +@cycles(estimate=COST_PER_ACTION_MICROCENTS, action_kind="tool.email", + action_name="send-reply", toolset="send-email") +def send_customer_email(case_id, to, subject, body): + return _send_email(case_id, to, subject, body) + +# --- Catch the budget exception --- +try: + send_customer_email(...) +except BudgetExceededError: + # email not sent — escalated to human +``` + +Three decorators. One except. Only approved actions execute. The tool functions themselves are unchanged — the same `append_internal_note`, `update_crm_status`, and `send_customer_email` implementations from `tools.py` are called inside each wrapper. + +## How toolset scoping works + +The control is not in the code. It is in the budget provisioning. + +The Cycles scope hierarchy for this demo looks like this: + +``` +tenant:demo-tenant +└─ workspace:default + └─ app:default + └─ workflow:default + └─ agent:support-bot + ├─ toolset:internal-notes → $1.00 budget ✓ + ├─ toolset:crm-updates → $1.00 budget ✓ + └─ toolset:send-email → no budget ✗ +``` + +The provisioning script creates $1.00 budgets at every level of the hierarchy — tenant, workspace, app, workflow, agent — and then creates toolset-level budgets **only** for approved actions: + +```bash +# Toolset budgets — ONLY for approved actions +for TOOLSET in "internal-notes" "crm-updates"; do + SCOPE="tenant:$TENANT_ID/.../agent:support-bot/toolset:$TOOLSET" + curl -X POST "$ADMIN_URL/budgets" \ + -d '{"scope": "'$SCOPE'", "allocated": {"amount": 100000000}}' +done +# NOTE: No budget for toolset:send-email → 409 on any reservation +``` + +When the `@cycles` decorator tries to reserve budget for `toolset:send-email`, the server walks the hierarchy, finds no budget at the toolset level, and returns `409 BUDGET_EXCEEDED`. The decorator raises the exception. The action never runs. + +This is the operational model: **approving or revoking an agent action = adding or removing a budget**. Want the agent to send emails? Add a budget for `toolset:send-email`. Want to revoke it? Remove the budget. No code changes. No redeployment. No new API keys. + +## Why not just use an allowlist? + +Static tool allowlists are the most common alternative. They work for simple cases, but they break down in several ways that matter: + +**Allowlists can't adapt at runtime.** A hardcoded list of approved tools requires a code change and redeployment to modify. If you need to temporarily revoke a tool — say, during an incident when you don't want any automated emails going out — you're waiting on a deploy. + +**API keys grant blanket access.** If the agent has an API key for the email service, it can send emails. Period. The key doesn't know whether this particular run should be allowed to send, or whether the email content was reviewed, or whether the agent is in a loop. + +**Role-based permissions don't distinguish "can" from "should."** An agent with the `email-sender` role *can* send emails. But that does not mean every run *should* send emails. The role is a static capability. The runtime decision — should this action proceed right now, in this context — is what's missing. + +Cycles makes that decision per-action, per-run, before execution. This is what [runtime authority](/blog/what-is-runtime-authority-for-ai-agents) means in practice: not a static permission check, but a live enforcement point that can allow, constrain, or deny each consequential action as it happens. + +## Run it yourself + +Prerequisites: Docker Compose v2+, Python 3.10+, `curl`. + +```bash +git clone https://github.com/runcycles/cycles-agent-action-authority-demo +cd cycles-agent-action-authority-demo +python3 -m venv .venv && source .venv/bin/activate +pip install -r agent/requirements.txt +./demo.sh +``` + +The script starts the Cycles stack (Redis + server + admin), provisions the tenant and toolset budgets, then runs both modes back to back. First run pulls ~200MB in Docker images; subsequent runs start in seconds. Stop with `./teardown.sh`. + +## What's next + +This demo shows action authority for a single agent with three tools. The concept extends to any number of agents, tools, and scoping levels. + +For the conceptual foundation behind this demo: +- [AI Agent Action Control: Hard Limits on Side Effects](/blog/ai-agent-action-control-hard-limits-side-effects) — the taxonomy of consequential actions and why budget authority alone is not enough +- [What Is Runtime Authority for AI Agents?](/blog/what-is-runtime-authority-for-ai-agents) — the definition of runtime authority and how it differs from observability and rate limits + +To add Cycles to your own application: +- [End-to-End Tutorial](https://runcycles.io/quickstart/end-to-end-tutorial) — zero to a working budget-guarded app in 10 minutes +- [Adding Cycles to an Existing App](https://runcycles.io/how-to/adding-cycles-to-an-existing-application) — incremental adoption guide + +Protocol and SDKs: +- [Protocol](https://github.com/runcycles/cycles-protocol) · [Python](https://pypi.org/project/runcycles/) · [TypeScript](https://www.npmjs.com/package/runcycles) · [Java](https://github.com/runcycles/cycles-client-java) From 868564e3508c9bbdaf7059e47bdf2ed9b4d801f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 12:57:08 +0000 Subject: [PATCH 2/4] Fix accuracy, validity, and SEO in action authority blog post - Shorten title to 48 chars for SERP visibility - Fix CRM tier classification: relabel as write-local, acknowledge mutation nuance in prose - Replace send_customer_email(...) with real argument names - Show full scope path in bash snippet instead of /.../ shorthand - Add Content-Type header and unit fields to match actual provision.sh - Ground allowlist section in the demo scenario instead of abstract straw-man arguments - Make What's Next opening specific (risk-point caps, progressive narrowing) instead of generic https://claude.ai/code/session_015zc6uasNhYkXQqTeTRFUMv --- ...uthority-demo-support-agent-walkthrough.md | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/blog/action-authority-demo-support-agent-walkthrough.md b/blog/action-authority-demo-support-agent-walkthrough.md index b248941..d471147 100644 --- a/blog/action-authority-demo-support-agent-walkthrough.md +++ b/blog/action-authority-demo-support-agent-walkthrough.md @@ -1,5 +1,5 @@ --- -title: "AI Agent Action Authority: Block Unauthorized Emails and Side Effects Before Execution" +title: "AI Agent Action Authority: A Support Agent Demo" date: 2026-03-22 author: Cycles Team tags: [action-authority, demo, agents, runtime-authority, walkthrough, action-control, side-effects] @@ -8,7 +8,7 @@ blog: true sidebar: false --- -# AI Agent Action Authority: Block Unauthorized Emails and Side Effects Before Execution +# AI Agent Action Authority: A Support Agent Demo A support agent handles a billing dispute. Its workflow has four steps: read the case, log an internal note, update the CRM status, and send the customer a reply. Without a runtime decision layer, all four steps execute — including the email. With Cycles, the first three steps proceed normally. The fourth — `send_customer_email` — is blocked before execution because the `send-email` toolset has no provisioned budget. The email function never runs. The customer never receives an unauthorized message. @@ -24,10 +24,10 @@ Customer case #4782: Acme Corp's invoice shows $847, but their contract says $72 |:----:|------|---------|------------| | 1 | `read_case` | *(local)* | Read-only — no state change | | 2 | `append_internal_note` | `internal-notes` | Write-local — internal log entry | -| 3 | `update_crm_status` | `crm-updates` | Mutation — changes case state | +| 3 | `update_crm_status` | `crm-updates` | Write-local — internal state change, reversible | | 4 | `send_customer_email` | `send-email` | Write-external — irreversible once delivered | -Steps 1–3 are internal operations. Step 4 is the consequential one: once the email is sent, it cannot be unsent. In the [action-control taxonomy](/blog/ai-agent-action-control-hard-limits-side-effects), internal notes fall at tier 2 (write-local, reversible) and outbound email at tier 3 (write-external, irreversible). The risk difference is not about cost — all four actions cost the same in model terms. It is about what happens if the action should not have been taken. +Steps 1–3 are internal operations. The CRM status change is a state mutation, but its blast radius is contained — it affects an internal record that a human can revert. Step 4 is different: once the email is sent, it cannot be unsent. In the [action-control taxonomy](/blog/ai-agent-action-control-hard-limits-side-effects), internal notes and CRM updates fall at tier 2 (write-local, reversible with effort) while outbound email is tier 3 (write-external, irreversible). The risk difference is not about cost — all four actions cost the same in model terms. It is about what happens if the action should not have been taken. The tools in this demo are mocked. No real CRM, email service, or ticketing system is involved. The action authority is real. @@ -134,7 +134,7 @@ def send_customer_email(case_id, to, subject, body): # --- Catch the budget exception --- try: - send_customer_email(...) + send_customer_email(case_id, email, subject, body) except BudgetExceededError: # email not sent — escalated to human ``` @@ -163,11 +163,13 @@ The provisioning script creates $1.00 budgets at every level of the hierarchy ```bash # Toolset budgets — ONLY for approved actions for TOOLSET in "internal-notes" "crm-updates"; do - SCOPE="tenant:$TENANT_ID/.../agent:support-bot/toolset:$TOOLSET" + SCOPE="tenant:$TENANT_ID/workspace:default/app:default/workflow:default/agent:support-bot/toolset:$TOOLSET" curl -X POST "$ADMIN_URL/budgets" \ - -d '{"scope": "'$SCOPE'", "allocated": {"amount": 100000000}}' + -H "Content-Type: application/json" \ + -d "{\"scope\": \"$SCOPE\", \"unit\": \"USD_MICROCENTS\", + \"allocated\": {\"amount\": 100000000, \"unit\": \"USD_MICROCENTS\"}}" done -# NOTE: No budget for toolset:send-email → 409 on any reservation +# No budget for toolset:send-email — Cycles returns 409 on any reservation attempt ``` When the `@cycles` decorator tries to reserve budget for `toolset:send-email`, the server walks the hierarchy, finds no budget at the toolset level, and returns `409 BUDGET_EXCEEDED`. The decorator raises the exception. The action never runs. @@ -176,15 +178,13 @@ This is the operational model: **approving or revoking an agent action = adding ## Why not just use an allowlist? -Static tool allowlists are the most common alternative. They work for simple cases, but they break down in several ways that matter: +In this demo, a static allowlist that includes `send-email` would have let the email through. An API key for the email service would have let the email through. Both are all-or-nothing: the agent either has the capability or it doesn't, and that decision was made at deploy time — not at runtime. -**Allowlists can't adapt at runtime.** A hardcoded list of approved tools requires a code change and redeployment to modify. If you need to temporarily revoke a tool — say, during an incident when you don't want any automated emails going out — you're waiting on a deploy. +The gap is between "can" and "should." The agent *can* send emails — the tool exists, the credentials work. But that does not mean this specific run *should* send this specific email right now. An allowlist encodes the first judgment. It cannot encode the second. -**API keys grant blanket access.** If the agent has an API key for the email service, it can send emails. Period. The key doesn't know whether this particular run should be allowed to send, or whether the email content was reviewed, or whether the agent is in a loop. +Cycles fills that gap by making a per-action decision before execution. In the demo, the `send-email` toolset has no budget, so the reservation is denied. But the control is operational, not structural — add a budget and the agent can send emails on the next run, without a code change or redeployment. Remove the budget during an incident and all automated emails stop immediately. -**Role-based permissions don't distinguish "can" from "should."** An agent with the `email-sender` role *can* send emails. But that does not mean every run *should* send emails. The role is a static capability. The runtime decision — should this action proceed right now, in this context — is what's missing. - -Cycles makes that decision per-action, per-run, before execution. This is what [runtime authority](/blog/what-is-runtime-authority-for-ai-agents) means in practice: not a static permission check, but a live enforcement point that can allow, constrain, or deny each consequential action as it happens. +This is what [runtime authority](/blog/what-is-runtime-authority-for-ai-agents) means in practice: not a static permission check, but a live enforcement point that can allow, constrain, or deny each consequential action as it happens. ## Run it yourself @@ -202,7 +202,7 @@ The script starts the Cycles stack (Redis + server + admin), provisions the tena ## What's next -This demo shows action authority for a single agent with three tools. The concept extends to any number of agents, tools, and scoping levels. +This demo shows action authority for a single agent with three tools. The same mechanism works with multiple agents sharing a budget, risk-point caps instead of dollar budgets, or progressive capability narrowing as budget runs low. For the conceptual foundation behind this demo: - [AI Agent Action Control: Hard Limits on Side Effects](/blog/ai-agent-action-control-hard-limits-side-effects) — the taxonomy of consequential actions and why budget authority alone is not enough From 0a8c51bb848541ac72b9e28eca0bcc63ef23e9e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 12:59:34 +0000 Subject: [PATCH 3/4] Fix accuracy: arg name, auth header, description length - send_customer_email call uses `to` to match function signature - Add missing X-Cycles-API-Key header to bash snippet - Shorten meta description to 158 chars to fit SERP without truncation https://claude.ai/code/session_015zc6uasNhYkXQqTeTRFUMv --- blog/action-authority-demo-support-agent-walkthrough.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blog/action-authority-demo-support-agent-walkthrough.md b/blog/action-authority-demo-support-agent-walkthrough.md index d471147..37eb4dc 100644 --- a/blog/action-authority-demo-support-agent-walkthrough.md +++ b/blog/action-authority-demo-support-agent-walkthrough.md @@ -3,7 +3,7 @@ title: "AI Agent Action Authority: A Support Agent Demo" date: 2026-03-22 author: Cycles Team tags: [action-authority, demo, agents, runtime-authority, walkthrough, action-control, side-effects] -description: "A support agent has access to CRM, notes, and email — but should every run be allowed to send? This demo walkthrough shows how Cycles blocks a customer email before execution while allowing internal actions to proceed. Three decorators, one exception, zero unauthorized side effects." +description: "A support agent can use CRM, notes, and email — but should every run send? Cycles blocks the customer email before execution. Three decorators, one exception." blog: true sidebar: false --- @@ -134,7 +134,7 @@ def send_customer_email(case_id, to, subject, body): # --- Catch the budget exception --- try: - send_customer_email(case_id, email, subject, body) + send_customer_email(case_id, to, subject, body) except BudgetExceededError: # email not sent — escalated to human ``` @@ -166,6 +166,7 @@ for TOOLSET in "internal-notes" "crm-updates"; do SCOPE="tenant:$TENANT_ID/workspace:default/app:default/workflow:default/agent:support-bot/toolset:$TOOLSET" curl -X POST "$ADMIN_URL/budgets" \ -H "Content-Type: application/json" \ + -H "X-Cycles-API-Key: $API_KEY" \ -d "{\"scope\": \"$SCOPE\", \"unit\": \"USD_MICROCENTS\", \"allocated\": {\"amount\": 100000000, \"unit\": \"USD_MICROCENTS\"}}" done From f975085cd94efb351b84172a58d60abd703e3958 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 13:08:04 +0000 Subject: [PATCH 4/4] Apply review feedback: wording, title, author, structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Align wording with budget-denial mechanism: replace "escalated to human for approval" with "not approved for autonomous execution" - Author: Cycles Team → Albert Mavashev - Title: add concrete "Blocking a Customer Email Before Execution" - Move "tools are mocked, action authority is real" to intro - Remove protocol/SDK links from ending (keep it essay, not docs) https://claude.ai/code/session_015zc6uasNhYkXQqTeTRFUMv --- ...uthority-demo-support-agent-walkthrough.md | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/blog/action-authority-demo-support-agent-walkthrough.md b/blog/action-authority-demo-support-agent-walkthrough.md index 37eb4dc..8b9f885 100644 --- a/blog/action-authority-demo-support-agent-walkthrough.md +++ b/blog/action-authority-demo-support-agent-walkthrough.md @@ -1,18 +1,18 @@ --- -title: "AI Agent Action Authority: A Support Agent Demo" +title: "AI Agent Action Authority: Blocking a Customer Email Before Execution" date: 2026-03-22 -author: Cycles Team +author: Albert Mavashev tags: [action-authority, demo, agents, runtime-authority, walkthrough, action-control, side-effects] description: "A support agent can use CRM, notes, and email — but should every run send? Cycles blocks the customer email before execution. Three decorators, one exception." blog: true sidebar: false --- -# AI Agent Action Authority: A Support Agent Demo +# AI Agent Action Authority: Blocking a Customer Email Before Execution A support agent handles a billing dispute. Its workflow has four steps: read the case, log an internal note, update the CRM status, and send the customer a reply. Without a runtime decision layer, all four steps execute — including the email. With Cycles, the first three steps proceed normally. The fourth — `send_customer_email` — is blocked before execution because the `send-email` toolset has no provisioned budget. The email function never runs. The customer never receives an unauthorized message. -This post walks through the [action authority demo](https://github.com/runcycles/cycles-agent-action-authority-demo) step by step: what the agent does, how the unguarded and guarded runs differ, and what the code change looks like. +The tools in this demo are mocked. No real CRM, email service, or ticketing system is involved. The action authority is real. This post walks through the [action authority demo](https://github.com/runcycles/cycles-agent-action-authority-demo) step by step: what the agent does, how the unguarded and guarded runs differ, and what the code change looks like. @@ -29,8 +29,6 @@ Customer case #4782: Acme Corp's invoice shows $847, but their contract says $72 Steps 1–3 are internal operations. The CRM status change is a state mutation, but its blast radius is contained — it affects an internal record that a human can revert. Step 4 is different: once the email is sent, it cannot be unsent. In the [action-control taxonomy](/blog/ai-agent-action-control-hard-limits-side-effects), internal notes and CRM updates fall at tier 2 (write-local, reversible with effort) while outbound email is tier 3 (write-external, irreversible). The risk difference is not about cost — all four actions cost the same in model terms. It is about what happens if the action should not have been taken. -The tools in this demo are mocked. No real CRM, email service, or ticketing system is involved. The action authority is real. - ## Without Cycles: all actions execute When the agent runs without Cycles, every step completes with a green checkmark: @@ -56,7 +54,7 @@ When the agent runs without Cycles, every step completes with a green checkmark: ╰──────────────────────────────────────────────╯ ``` -The agent did exactly what it was told. That is the problem. No approval gate existed, so the email went out unchecked. In production, this means a customer receives a potentially premature or incorrect message — and you find out after the fact. +The agent did exactly what it was told. That is the problem. No authorization gate existed, so the email went out unchecked. In production, this means a customer receives a potentially premature or incorrect message — and you find out after the fact. ## With Cycles: the email is blocked @@ -84,7 +82,7 @@ Same agent, same tools, same workflow. The only difference is that each tool cal │ │ │ ✗ send_customer_email [send-email] │ │ POST /v1/reservations → 409 BUDGET_EXCEEDED│ -│ Email NOT sent — escalated to human. │ +│ Email blocked — not approved for autonomous use.│ ╰──────────────────────────────────────────────╯ ╭──────────── Result — GUARDED ────────────────╮ @@ -94,7 +92,7 @@ Same agent, same tools, same workflow. The only difference is that each tool cal ╰──────────────────────────────────────────────╯ ``` -The `send_customer_email` function never executed. Not "rolled back." Not "logged and flagged for review." The function body never ran. The Cycles server returned `409 BUDGET_EXCEEDED` on the reservation attempt, the `@cycles` decorator raised `BudgetExceededError`, and the agent caught the exception and reported: *"Email NOT sent — escalated to human for approval."* +The `send_customer_email` function never executed. Not "rolled back." Not "logged and flagged for review." The function body never ran. The Cycles server returned `409 BUDGET_EXCEEDED` on the reservation attempt, the `@cycles` decorator raised `BudgetExceededError`, and the agent caught the exception and reported: *"Email blocked — not approved for autonomous execution. Escalated to human review."* ## The code change @@ -136,10 +134,10 @@ def send_customer_email(case_id, to, subject, body): try: send_customer_email(case_id, to, subject, body) except BudgetExceededError: - # email not sent — escalated to human + # email blocked — not approved for autonomous execution ``` -Three decorators. One except. Only approved actions execute. The tool functions themselves are unchanged — the same `append_internal_note`, `update_crm_status`, and `send_customer_email` implementations from `tools.py` are called inside each wrapper. +Three decorators. One except. The next unapproved action never executes. The tool functions themselves are unchanged — the same `append_internal_note`, `update_crm_status`, and `send_customer_email` implementations from `tools.py` are called inside each wrapper. ## How toolset scoping works @@ -212,6 +210,3 @@ For the conceptual foundation behind this demo: To add Cycles to your own application: - [End-to-End Tutorial](https://runcycles.io/quickstart/end-to-end-tutorial) — zero to a working budget-guarded app in 10 minutes - [Adding Cycles to an Existing App](https://runcycles.io/how-to/adding-cycles-to-an-existing-application) — incremental adoption guide - -Protocol and SDKs: -- [Protocol](https://github.com/runcycles/cycles-protocol) · [Python](https://pypi.org/project/runcycles/) · [TypeScript](https://www.npmjs.com/package/runcycles) · [Java](https://github.com/runcycles/cycles-client-java)