<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Rustem Feyzkhanov — Blog</title><description>Writing on AI platforms, agent authorization, and production AI.</description><link>https://www.ryfeus.io</link><language>en-us</language><item><title>Distributed Agents, Centralized Authorization</title><link>https://www.ryfeus.io/blog/distributed-agents-centralized-authorization</link><guid isPermaLink="true">https://www.ryfeus.io/blog/distributed-agents-centralized-authorization</guid><description>How I put OpenAPPA policy checks in front of Claude Managed Agents tool calls, outside the sandbox, with human approval from Web or Slack.</description><pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;How I put OpenAPPA policy checks in front of Claude Managed Agents tool calls: outside the sandbox, with human approval from Web or Slack.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;An agent finishes a meeting summary and wants to email it. If every recipient is at acme.com, it should just send. If one of them is a customer at partner.io, a person should look first.&lt;/p&gt;
&lt;p&gt;That split is the interesting part of agent authorization. It gets harder in the gap between asking and answering. While the approval sits in Slack, the user may press Stop, an admin may tighten the policy, or the agent may add one more recipient. So when a teammate finally clicks &lt;strong&gt;Allow&lt;/strong&gt;, what did that click approve?&lt;/p&gt;
&lt;p&gt;With managed agents this is not an edge case. The harness, the sandbox, the Web UI and Slack all run in different places, and none of them alone knows enough to answer that question. I built a reference application to find out where the answer should live. Short version: in the application controller, with &lt;a href=&quot;https://www.openappa.com/how-it-works&quot;&gt;OpenAPPA&lt;/a&gt; making the policy call and &lt;a href=&quot;https://platform.claude.com/docs/en/managed-agents/permission-policies&quot;&gt;Claude Managed Agents&lt;/a&gt; (CMA) holding the tool call until the controller says go.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.ryfeus.io/assets/blog/openappa-cma/email-approval-hero.png&quot; alt=&quot;An external email waits for approval: CMA pauses send_email to dana@acme.com and pat@partner.io, the controller asks OpenAPPA, a human approves in Web or Slack, and the controller revalidates the exact call before CMA sends it. Internal-only emails go straight through.&quot; width=&quot;1280&quot; height=&quot;720&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-setup&quot;&gt;The setup&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/tree/3dc12952dc9d4c1efc00bc1bfb296859b95b5488&quot;&gt;reference app&lt;/a&gt; lets you continue the same agent conversation from a web UI or a Slack thread. Three pieces matter here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CMA&lt;/strong&gt; runs the agent and keeps the canonical transcript and tool arguments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The application controller&lt;/strong&gt; coordinates durable tasks: it reconciles pending tool calls, task state and human responses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenAPPA&lt;/strong&gt; is the policy engine, embedded inside the controller as a pinned native Rust helper.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I put the policy check in the controller because it was already the one place that reconciles all of that state. Adding a separate authorization service would have meant rebuilding the same coordination twice.&lt;/p&gt;
&lt;h2 id=&quot;a-tool-allowlist-is-not-a-policy&quot;&gt;A tool allowlist is not a policy&lt;/h2&gt;
&lt;p&gt;“The agent may send email” says nothing about to whom, with what content, on whose behalf or under what conditions.&lt;/p&gt;
&lt;p&gt;Real rules depend on context. The &lt;a href=&quot;https://aws.amazon.com/blogs/opensource/introducing-the-dogwood-local-engine-temporal-governance-for-agent-actions/&quot;&gt;Dogwood Local Engine introduction&lt;/a&gt; gives a good example: allow a repository push only if tests passed in the last fifteen minutes with no failure since. That needs recorded events and timing. Reading an internal document may restrict what the agent can disclose later, and approving one action should not lift that restriction.&lt;/p&gt;
&lt;p&gt;I find it useful to treat two choices separately: what the policy language can express, and where the check is enforced. Engines differ mainly on the first:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Approach&lt;/th&gt;&lt;th&gt;What it reasons over&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://docs.cedarpolicy.com/auth/authorization.html&quot;&gt;Cedar&lt;/a&gt; / &lt;a href=&quot;https://www.openpolicyagent.org/docs/external-data&quot;&gt;OPA&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Rules over the authorization context you supply, including facts derived from workflow history&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Dogwood&lt;/td&gt;&lt;td&gt;Recorded events: ordering, time windows, counts&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OpenAPPA&lt;/td&gt;&lt;td&gt;Tool contracts, security labels, information-flow restrictions and policy-permitted remedies such as human approval&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Agent auto-modes&lt;/td&gt;&lt;td&gt;Model judgment about proposed actions, such as &lt;a href=&quot;https://code.claude.com/docs/en/permission-modes&quot;&gt;Claude Code’s classifier review&lt;/a&gt;; complements the others&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;OpenAPPA publishes comparisons with &lt;a href=&quot;https://www.openappa.com/openappa-vs-cedar&quot;&gt;Cedar&lt;/a&gt;, &lt;a href=&quot;https://www.openappa.com/openappa-vs-dogwood&quot;&gt;Dogwood&lt;/a&gt;, &lt;a href=&quot;https://www.openappa.com/openappa-vs-opa&quot;&gt;OPA&lt;/a&gt; and &lt;a href=&quot;https://www.openappa.com/openappa-vs-auto-mode&quot;&gt;auto-modes&lt;/a&gt;. They are worth reading, as the vendor’s case for its design. The rest of this post is about the second choice: where enforcement goes.&lt;/p&gt;
&lt;h2 id=&quot;why-internal-email-runs-and-external-email-waits&quot;&gt;Why internal email runs and external email waits&lt;/h2&gt;
&lt;p&gt;Human attention is the scarcest thing in an approval system. Ask for it on every email and people start clicking Allow without reading, which is worse than no approval at all. So the useful question per action is not “is this risky?” but “is this worth a person’s attention?”&lt;/p&gt;
&lt;p&gt;For email, the recipient domain is a good proxy:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Every recipient @acme.com&lt;/th&gt;&lt;th&gt;Any other recipient&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Who can read it&lt;/td&gt;&lt;td&gt;People already under your confidentiality, retention and DLP controls&lt;/td&gt;&lt;td&gt;Anyone, and you can’t recall it&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cost of a mistake&lt;/td&gt;&lt;td&gt;Awkward, usually fixable&lt;/td&gt;&lt;td&gt;Data disclosure, or a commitment made in the company’s name&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Payoff for a prompt injection&lt;/td&gt;&lt;td&gt;Low: the attacker isn’t on the list&lt;/td&gt;&lt;td&gt;High: “forward this to me” is the classic exfiltration path&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Policy&lt;/td&gt;&lt;td&gt;Allow automatically&lt;/td&gt;&lt;td&gt;A person approves this exact email&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The pattern generalizes beyond email. Reversible actions inside your boundary run automatically; irreversible ones that cross it wait for a person; tools the policy doesn’t know about are denied.&lt;/p&gt;
&lt;h2 id=&quot;put-the-decision-where-the-context-already-lives&quot;&gt;Put the decision where the context already lives&lt;/h2&gt;
&lt;p&gt;The controller already knows whether the task is still active and which provider event is pending. The application knows whether the person responding owns the conversation. The controller loads the canonical call details from CMA, never from the sandbox or the UI, and sends OpenAPPA trusted context: event identifiers, application identity, tool name and canonical arguments.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.ryfeus.io/assets/blog/openappa-cma/authorization-boundary.png&quot; alt=&quot;The controller combines canonical CMA state with trusted task and user context, calls OpenAPPA and receives authorized human rulings from Web or Slack. Only the controller confirms or rejects the call.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The CMA box shows managed execution at the application boundary, not CMA’s internal harness or sandbox topology.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A gateway could make the same decision, if you kept the same context current there. Reusing the controller avoids duplicating that coordination. Leases let many scheduler workers act as one logical authorization point without re-reading the whole transcript.&lt;/p&gt;
&lt;p&gt;One nice side effect: because CMA holds the call, the sandbox never needs an authorization token for this approval path. Normal credentials and service authentication still apply everywhere else.&lt;/p&gt;
&lt;h2 id=&quot;wiring-openappa-into-the-cma-checkpoint&quot;&gt;Wiring OpenAPPA into the CMA checkpoint&lt;/h2&gt;
&lt;p&gt;CMA pauses any tool configured with &lt;code&gt;permission_policy: {type: always_ask}&lt;/code&gt;. The application then answers with &lt;code&gt;user.tool_confirmation&lt;/code&gt;: allow or deny for that pending event (&lt;a href=&quot;https://platform.claude.com/docs/en/managed-agents/permission-policies&quot;&gt;docs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;In the repo’s &lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/blob/3dc12952dc9d4c1efc00bc1bfb296859b95b5488/cma/agents/application.md&quot;&gt;agent config&lt;/a&gt;, the governed tools are CMA’s built-in &lt;code&gt;web_fetch&lt;/code&gt; and &lt;code&gt;web_search&lt;/code&gt;; other built-ins stay &lt;code&gt;always_allow&lt;/code&gt; and never reach the adapter. An email tool would arrive through an MCP server instead. CMA’s MCP toolsets pause by default, and the controller already maps those calls to &lt;code&gt;mcp/&amp;lt;server&amp;gt;/&amp;lt;tool&amp;gt;&lt;/code&gt; before asking OpenAPPA. A paused call doesn’t have to mean a human: the controller confirms it automatically when policy allows.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/blob/3dc12952dc9d4c1efc00bc1bfb296859b95b5488/backend/src/managed_agents_app/policy/openappa.py&quot;&gt;Python adapter&lt;/a&gt; sends JSON over stdin to the Rust helper. Each evaluation starts fresh in-memory OpenAPPA state, replays &lt;code&gt;SessionStart&lt;/code&gt; and &lt;code&gt;ToolCall&lt;/code&gt;, and returns allow, deny or an offer of human review. Any failure, or any policy feature the adapter doesn’t support, leaves the call paused. It fails closed.&lt;/p&gt;
&lt;p&gt;Here is the email rule as OpenAPPA policy. Contracts are checked in order, and the first match wins. An argument selector on an array matches only when every element matches, so a single external address drops the call to the second contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Every recipient on the company domain: allow automatically.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[[&lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tool&lt;/span&gt;&lt;span&gt;]]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name = &lt;/span&gt;&lt;span&gt;&quot;mcp/mail/send_email(to:*@acme.com)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;parameters = { type = &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;, properties = { to = { type = &lt;/span&gt;&lt;span&gt;&quot;array&quot;&lt;/span&gt;&lt;span&gt;, minItems = &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, items = { type = &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; } }, subject = { type = &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; }, body = { type = &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; } }, required = [&lt;/span&gt;&lt;span&gt;&quot;to&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;subject&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;body&quot;&lt;/span&gt;&lt;span&gt;] }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;delta = {}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Anything else: a person approves this exact email.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[[&lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tool&lt;/span&gt;&lt;span&gt;]]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name = &lt;/span&gt;&lt;span&gt;&quot;mcp/mail/send_email&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;parameters = { type = &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;, properties = { to = { type = &lt;/span&gt;&lt;span&gt;&quot;array&quot;&lt;/span&gt;&lt;span&gt;, minItems = &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, items = { type = &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; } }, subject = { type = &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; }, body = { type = &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; } }, required = [&lt;/span&gt;&lt;span&gt;&quot;to&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;subject&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;body&quot;&lt;/span&gt;&lt;span&gt;] }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;requires = { attention = [&lt;/span&gt;&lt;span&gt;&quot;user-approval&quot;&lt;/span&gt;&lt;span&gt;] }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;delta = {}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[[&lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;authority&lt;/span&gt;&lt;span&gt;]]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name = &lt;/span&gt;&lt;span&gt;&quot;user&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;hint = &lt;/span&gt;&lt;span&gt;&quot;Ask the user to approve this exact email.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;authority&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;permits&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;attention = [&lt;/span&gt;&lt;span&gt;&quot;user-approval&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;externals&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;authorities&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;builtin = &lt;/span&gt;&lt;span&gt;&quot;hitl&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;user&lt;/code&gt; authority can satisfy exactly one requirement, &lt;code&gt;user-approval&lt;/code&gt;, for this call. It cannot override any other denial. The full policy also keeps the repo’s deny-undeclared wildcard, so unknown tools are refused.&lt;/p&gt;
&lt;p&gt;This policy isn’t in the repo. I ran it through the same pinned OpenAPPA helper the app ships, built from the public repo:&lt;/p&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Call&lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;to: [dana@acme.com, lee@acme.com]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Allowed automatically&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;to: [dana@acme.com, pat@partner.io]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Human review&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;to: [x@acme.com.evil.io]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Human review&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Approved, then the agent adds &lt;code&gt;leak@rival.com&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;stale_review&lt;/code&gt;: the approval doesn’t carry over&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Extra &lt;code&gt;cc&lt;/code&gt; field, or &lt;code&gt;to&lt;/code&gt; sent as one string&lt;/td&gt;&lt;td&gt;Denied by the schema&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;to: [&quot;pat@partner.io, dana@acme.com&quot;]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Allowed, and that’s a hole&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two lessons came out of those rows. First, declare the schema. Without &lt;code&gt;parameters&lt;/code&gt;, a &lt;code&gt;cc: [pat@partner.io]&lt;/code&gt; sailed through as internal mail; OpenAPPA’s schemas reject unknown fields by default, so adding one closed it. Second, selectors match strings, they don’t parse email addresses, and OpenAPPA’s schema subset has no regex. The mail tool itself has to reject anything that isn’t exactly one address per element. Policy and tool must agree on what a recipient is.&lt;/p&gt;
&lt;h2 id=&quot;a-human-click-is-an-input-to-policy-not-a-bypass&quot;&gt;A human click is an input to policy, not a bypass&lt;/h2&gt;
&lt;p&gt;Take the summary going to &lt;a href=&quot;mailto:dana@acme.com&quot;&gt;dana@acme.com&lt;/a&gt; and &lt;a href=&quot;mailto:pat@partner.io&quot;&gt;pat@partner.io&lt;/a&gt;. CMA pauses the call. OpenAPPA offers human review. The controller persists a review request whose fingerprint covers the call identity, its arguments, the policy and the authority.&lt;/p&gt;
&lt;p&gt;The task moves to &lt;code&gt;INPUT_REQUIRED&lt;/code&gt; over A2A. Web and Slack show the same pending decision: the tool, the exact recipients, subject and body, and the policy’s explanation, only to an authorized responder. Reload the browser and it is still there.&lt;/p&gt;
&lt;p&gt;When someone clicks Allow, the controller does not just forward it to CMA. It reloads the current call, starts a fresh OpenAPPA process, rebuilds the offered remedy, checks it still binds to the same call, policy and authority, and runs the ruling through OpenAPPA’s own control path. Allow must come back as native &lt;code&gt;Authorized&lt;/code&gt; for the same tool and arguments; Deny as native &lt;code&gt;Declined&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.ryfeus.io/assets/blog/openappa-cma/decision-paths.png&quot; alt=&quot;Three outcomes, one controlled release path: OpenAPPA allows, denies or offers review. Review persists a request, takes an authorized ruling and revalidates it with fresh OpenAPPA. Every outcome passes a durable fenced decision before CMA confirmation or rejection.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That answers the opening question:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The user pressed Stop.&lt;/strong&gt; The controller commits a decision only with an active task, a valid worker lease and no cancellation. A late Allow cannot release cancelled work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy or recipients changed.&lt;/strong&gt; The fingerprint no longer matches. The review is invalidated and gets a new request ID, so approving an email to Pat never covers an email to Pat and a competitor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two people clicked.&lt;/strong&gt; The first authorized ruling wins. A duplicate Allow adds nothing; a late conflicting Deny cannot replace it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The confirmation was lost in transit.&lt;/strong&gt; The &lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/blob/3dc12952dc9d4c1efc00bc1bfb296859b95b5488/backend/src/managed_agents_app/cma_controller/scheduler.py&quot;&gt;controller&lt;/a&gt; never blindly retries. If CMA may have accepted it, it waits for canonical evidence.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One honest caveat: CMA and the database share no atomic transaction. These checks make duplicate confirmations unlikely; they do not guarantee exactly-once external effects.&lt;/p&gt;
&lt;h2 id=&quot;nothing-waits-on-an-open-connection&quot;&gt;Nothing waits on an open connection&lt;/h2&gt;
&lt;p&gt;Because the review is persisted, no HTTP request or process has to stay alive while a person thinks. When a response arrives from either interface, any worker can rebuild the state, even after a restart. OpenAPPA evaluations are stateless; the approval is durable. Aurora DSQL stores review metadata and decisions, CMA keeps the canonical tool arguments, and private temporary object storage holds the review text.&lt;/p&gt;
&lt;p&gt;Signed CMA webhooks wake the scheduler, and a 30-second reconciler catches anything missed (&lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/blob/3dc12952dc9d4c1efc00bc1bfb296859b95b5488/docs/architecture.md&quot;&gt;architecture&lt;/a&gt;). Optional streaming previews show progress but never authorize a tool or decide task state.&lt;/p&gt;
&lt;h2 id=&quot;how-its-tested-and-what-that-proves&quot;&gt;How it’s tested, and what that proves&lt;/h2&gt;
&lt;p&gt;The deterministic suite runs the real pinned OpenAPPA helper against a fake CMA, recorded Slack APIs and local PostgreSQL:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/blob/3dc12952dc9d4c1efc00bc1bfb296859b95b5488/e2e/human-review.spec.ts&quot;&gt;Browser tests&lt;/a&gt;: pending reviews send no provider confirmation, survive a reload and settle through Allow or Deny; unauthorized Slack responses, duplicate Allow and late Deny are rejected.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/blob/3dc12952dc9d4c1efc00bc1bfb296859b95b5488/backend/tests/test_openappa_policy.py&quot;&gt;Native policy&lt;/a&gt; and &lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/blob/3dc12952dc9d4c1efc00bc1bfb296859b95b5488/backend/tests/integration/test_cma_controller_postgres.py&quot;&gt;database tests&lt;/a&gt;: stale approval and cancellation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A separate sandbox run against real CMA on October 31 exercised Web reload and Allow, Slack Deny and duplicates, and Stop on a pending review. Signed Slack interactions still don’t prove native Slack-client clicks or rendering.&lt;/p&gt;
&lt;h2 id=&quot;what-this-adapter-does-not-cover&quot;&gt;What this adapter does not cover&lt;/h2&gt;
&lt;p&gt;Only calls that pause and reach the adapter are governed. The &lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/blob/3dc12952dc9d4c1efc00bc1bfb296859b95b5488/services/openappa/README.md&quot;&gt;supported scope&lt;/a&gt; is per-call authorization with durable human review and exact-call revalidation. The broader &lt;a href=&quot;https://www.openappa.com/how-it-works&quot;&gt;OpenAPPA model&lt;/a&gt; goes further than this integration. Tool results, full information-flow history, sanitization, sandbox networking and &lt;code&gt;always_allow&lt;/code&gt; tools stay outside it; enforcing data flow end to end would need more integration points and state. And the agent’s &lt;code&gt;ask_user&lt;/code&gt; tool is for clarification only: it grants no authorization.&lt;/p&gt;
&lt;p&gt;For email, that gap matters. The recipient isn’t the whole story: if the agent just read the salary spreadsheet, even an internal colleague may be the wrong reader. OpenAPPA can express this with audience labels: reading restricted data limits who may receive anything the agent sends afterwards (&lt;a href=&quot;https://github.com/archestra-ai/OpenAPPA/tree/c7c1e1ef36aa07604b421c48be266d7397557656/examples/tests/secret-stays-inside&quot;&gt;example&lt;/a&gt;). A per-call adapter like this one doesn’t carry that history yet, so today the recipient rule is the whole check.&lt;/p&gt;
&lt;h2 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;/h2&gt;
&lt;p&gt;Distributed execution can still have one authoritative authorization path. You need three things: a provider that enforces a pause before the tool runs, a controller with trusted context, and release authority that lives only in that controller. It evaluates policy, persists any human input and revalidates the exact call before release. Event delivery alone guarantees nothing.&lt;/p&gt;
&lt;p&gt;Then spend human attention only where an action is irreversible or leaves your boundary, and make sure the approval is bound to the exact call. Before you add a new authorization service to your agent stack, check whether your controller already has the task context, identity and recovery logic. Mine did.&lt;/p&gt;
&lt;p&gt;How are you handling approvals for agent tool calls today: in a gateway, in the harness, or in your application? I’d like to hear what’s working.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Code: public commit &lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/tree/3dc12952dc9d4c1efc00bc1bfb296859b95b5488&quot;&gt;&lt;code&gt;3dc1295&lt;/code&gt;&lt;/a&gt;, merged in &lt;a href=&quot;https://github.com/ryfeus/managed-agent-web-slack/pull/1&quot;&gt;PR #1&lt;/a&gt; on November 1, 2026. A reference implementation, not a production-readiness claim.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;#AIAgents #AgentSecurity #Authorization #Anthropic #AWS&lt;/p&gt;</content:encoded></item></channel></rss>