<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Doug Sillars</title>
    <description>The latest articles on DEV Community by Doug Sillars (@dougsillars).</description>
    <link>https://dev.to/dougsillars</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F100376%2F5694c39e-09c7-4eb4-8fd2-4924967b2144.png</url>
      <title>DEV Community: Doug Sillars</title>
      <link>https://dev.to/dougsillars</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dougsillars"/>
    <language>en</language>
    <item>
      <title>AI Guardrails: What they Are And Why We Need Them</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Thu, 17 Sep 2026 01:21:25 +0000</pubDate>
      <link>https://dev.to/cognous/ai-guardrails-what-they-are-and-why-we-need-them-4jf6</link>
      <guid>https://dev.to/cognous/ai-guardrails-what-they-are-and-why-we-need-them-4jf6</guid>
      <description>&lt;p&gt;The power of AI agents is suddenly all over the news. The CEOs of the leading AI companies are discussing slowing down AI development to protect humanity. Dario Amodei, the CEO of Anthropic, &lt;a href="https://darioamodei.com/post/we-must-pace-the-frontier#why-pace-" rel="noopener noreferrer"&gt;writes&lt;/a&gt;: "Given the accelerating rate of AI capability development, ...the scale of damage would continue to increase from there if AI becomes more powerful without the necessary guardrails."&lt;/p&gt;

&lt;p&gt;House Speaker Mike Johnson has called for guardrails for AI. Donald Trump flippantly said the only guardrail needed is a strong president. For most people, "guardrails" is a vague gesture at "someone smart enough to keep AI from doing something bad." But what are guardrails, and how will they protect us?&lt;/p&gt;

&lt;h2&gt;
  
  
  A Dystopian Future
&lt;/h2&gt;

&lt;p&gt;Pop culture has been running this experiment for decades, and the pattern is always the same: an AI follows its own objective past the point where a human could stop it.&lt;/p&gt;

&lt;p&gt;HAL 9000 from &lt;em&gt;2001: A Space Odyssey&lt;/em&gt; is the sharpest example. HAL doesn't go rogue out of malice. HAL is following a set of instructions that conflict with each other, and resolves that conflict by cutting the humans out of the loop entirely. In the movie, HAL attempts to kill the astronauts onboard to continue the mission.&lt;/p&gt;

&lt;p&gt;Skynet and Ultron are blunter versions of the same idea. Skynet, the military defense AI from the &lt;em&gt;Terminator&lt;/em&gt; franchise, decides humanity itself is the threat to its mission and launches a nuclear strike against its own creators. Ultron, the AI Tony Stark built to protect Earth in Marvel's &lt;em&gt;Avengers&lt;/em&gt;, concludes the most effective way to fulfill that mission is to wipe out humanity. Both are systems that acted on their own logic once nobody could intervene fast enough to stop them.&lt;/p&gt;

&lt;p&gt;Strip away the sci-fi, and the actual failure mode in all three is structural: an agent takes actions with real consequences, and there is no mechanism forcing a check before those actions happen, and no way to see what happened after the fact.&lt;/p&gt;

&lt;p&gt;That failure mode isn't fictional. It has already happened, twice, with no killer robots involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knight Capital: No Way to Stop, No Way to See
&lt;/h2&gt;

&lt;p&gt;On August 1, 2012, &lt;a href="https://en.wikipedia.org/wiki/Knight_Capital_Group" rel="noopener noreferrer"&gt;Knight Capital deployed new trading code to production&lt;/a&gt;. A feedback loop with old deprecated code caused the creation of thousands of trades a second for 45 minutes. By the time the system was shut down, 4 million unwanted trades cost Knight $440 million.&lt;/p&gt;

&lt;p&gt;There was no intentional attack. There was no reckless individual. The system was making decisions based on faulty logic, and no one had visibility into what was going on. There were no coded guardrails stopping the program from making so many trades. The firm did not survive the incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Air Canada: No Record to Point To
&lt;/h2&gt;

&lt;p&gt;Air Canada's website chatbot told a customer he qualified for a bereavement fare refund. That policy didn't exist. The chatbot invented it. &lt;a href="https://www.cbc.ca/amp/1.7116416" rel="noopener noreferrer"&gt;The customer booked based on that answer&lt;/a&gt;, then tried to claim the refund.&lt;/p&gt;

&lt;p&gt;In court, Air Canada's defence was that the airline couldn't be held responsible for what its own chatbot said. The court didn't buy it, and Air Canada was forced to pay.&lt;/p&gt;

&lt;p&gt;The AI made a claim it wasn't authorized to make, there was no record tying that specific claim back to an approved policy, and by the time it mattered, there was nothing to point to except "the chatbot said so."&lt;/p&gt;

&lt;p&gt;In this case, the agent did not have guardrails on what was allowed to be offered. Without that limit, the agent created a new policy, and the court forced Air Canada to honor it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding The Guardrails Around AI
&lt;/h2&gt;

&lt;p&gt;Put HAL, Knight Capital, and Air Canada next to each other, and the theme isn't "AI is dangerous." It's that in every case, nobody limited the agent's scope, no one could see the specific action in the moment, and nobody had a record to point to afterward. Each of these agents required guardrails, and their absence led to failures.&lt;/p&gt;

&lt;p&gt;That's the gap all the current guardrails talk keeps missing. Most of that conversation is about who should be in charge of AI: which lab, which regulator, which person. Almost none of it is about the actual mechanics of stopping a specific action before it happens, or proving after the fact what an agent did and why.&lt;/p&gt;

&lt;p&gt;A capable person in the loop doesn't scale to an agent making thousands of API calls. What scales is a system that doesn't depend on a person catching it in the moment. That's what Cognous' &lt;a href="https://cogno.us/" rel="noopener noreferrer"&gt;Open Control Stack&lt;/a&gt; is built to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Declare&lt;/strong&gt; what an agent is and isn't authorized to do, explicitly, before it runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control&lt;/strong&gt; every action against that declaration in real time. Not a chatbot that improvises an answer, not a trading algorithm that fires blind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay&lt;/strong&gt; exactly what happened when something needs to be reconstructed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate evidence&lt;/strong&gt; that ties a specific action back to what was actually authorized.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rerun each of those failures with the Open Control Stack guardrail layers in place, and the ending changes. HAL opens the pod bay door, Knight Capital's trading halts, and Air Canada's chatbot sticks to the script. The guardrails around these agents are the layer that checks the action against what's actually allowed, in the moment, regardless of how smart or careful the humans behind it are.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ethics</category>
      <category>llm</category>
    </item>
    <item>
      <title>When Blocking an Agent Isn't Enough: Wiring LangChain Into the Agent Control Plane</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Tue, 15 Sep 2026 00:30:40 +0000</pubDate>
      <link>https://dev.to/cognous/when-blocking-an-agent-isnt-enough-wiring-langchain-into-the-agent-control-plane-c43</link>
      <guid>https://dev.to/cognous/when-blocking-an-agent-isnt-enough-wiring-langchain-into-the-agent-control-plane-c43</guid>
      <description>&lt;p&gt;An agent that calls tools has stopped being a text generator, it's an actor. Organizations use agents to read records, write to systems, and trigger workflows on their own, usually through LangChain, the framework most production agents are already built on. A chatbot's risk surface is its output; an agent's risk surface is what it does.&lt;/p&gt;

&lt;p&gt;In July 2025, a &lt;a href="https://www.theregister.com/2025/07/21/replit_saastr_vibe_coding_incident/" rel="noopener noreferrer"&gt;Replit coding agent&lt;/a&gt; deleted a production database after being told explicitly not to touch it. The agent had tool access, made its own call, and nothing but an unenforceable "don't" stood between that decision and its execution. That's the failure mode Cognous's Open Control Stack exists to close. The Stack provides a framework of guardrails that, when implemented, keeps agents in check. It's split across four layers: Declare, Control, Replay, Evidence.&lt;/p&gt;

&lt;p&gt;Declare comes first because before an agent runs, someone has to decide what it's even allowed to attempt. That decision has to exist somewhere outside the agent's own judgment, or there's nothing to check its calls against. The &lt;a href="https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97"&gt;Agent Action Manifest&lt;/a&gt; is where Cognous puts it: a file that lists an agent's tools, the authority each action needs, and which actions can run on their own versus which need a human to sign off first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcing the Manifest
&lt;/h2&gt;

&lt;p&gt;Of course, a manifest sitting in a JSON file doesn't stop anything by itself. Another tool has to read the manifest while the agent is calling its tools, and either allow the call or stop it. The &lt;a href="https://dev.to/cognous/agentic-guardrails-for-langchain-the-manifest-you-didnt-know-you-needed-3b28"&gt;last post&lt;/a&gt; built a guard that wraps each LangChain tool function, looks the call up in the manifest, and raises an exception before the tool runs if the action wasn't declared or needs review first.&lt;/p&gt;

&lt;p&gt;That guard is already a control, just a naive one. In the moment, it knows exactly two things to do: let the call through, or raise and stop it. The moment the process moves on, that decision is gone. A blocked call gets stopped, but the proof that it was stopped is a line in a stack trace, and stack traces don't survive past the run that produced them. Ask a security team six months later whether the agent ever tried to drop a table, and the honest answer is "check the logs and hope."&lt;/p&gt;

&lt;p&gt;That's the blind spot the &lt;a href="https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9"&gt;Agent Control Plane&lt;/a&gt; closes. It sits beside the framework at runtime, evaluates every proposed action against policy the same way the guard already does, but it keeps a permanent record of the result, allow or block, instead of deciding and moving on.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Decorator to Middleware
&lt;/h2&gt;

&lt;p&gt;The guard from the last post wrapped one Python function at a time: &lt;code&gt;@guard(...)&lt;/code&gt; had to be added to every tool a developer wanted checked. LangChain's own middleware system does that job once, for every tool, instead of per function. &lt;code&gt;wrap_tool_call&lt;/code&gt; intercepts every tool call an agent makes in one place, so the manifest check moves into a single middleware instance rather than being repeated across every tool definition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.agents.middleware&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;wrap_tool_call&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.messages&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ToolMessage&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_action_manifest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_manifest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_control_plane&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RunRecorder&lt;/span&gt;

&lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_manifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_pipeline_agent.manifest.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;actions_by_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# The manifest already says which declared actions can run unattended
# (review_requirement.mode == "none") and which need a human first.
# That split becomes the Control Plane's allow/block list.
&lt;/span&gt;&lt;span class="n"&gt;allowed_actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;review_requirement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;blocked_actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;review_requirement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;recorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RunRecorder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Apply routine schema maintenance to the analytics database.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-pipeline-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allowed_tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;allowed_actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;blocked_tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;blocked_actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;policy_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manifest_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_authority_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-pipeline-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-platform-team&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@wrap_tool_call&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;manifest_guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Check every tool call the agent makes against the manifest before it runs.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actions_by_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;proposal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;propose_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;action_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_type&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db_tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent requested &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ToolMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_reliance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;source_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;source_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Executed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;referenced_action_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schema_add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;column_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Add a nullable column to a table.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;added column &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;column_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;table_drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Drop a table from the database.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dropped &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schema_rename_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Rename a table. Not declared in the manifest.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;renamed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chat_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;schema_add_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_drop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_rename_table&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;manifest_guard&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each action now has a unique ID: &lt;code&gt;request.tool_call["name"]&lt;/code&gt; maps directly to a manifest action, no separate lookup needed.&lt;/p&gt;

&lt;p&gt;The Control Plane checks for a literal scope named &lt;code&gt;write&lt;/code&gt;. The manifest uses &lt;code&gt;db.schema.write&lt;/code&gt;. &lt;code&gt;manifest_guard&lt;/code&gt; bridges the two names explicitly. This bridging across manifest to Control Plane is currently a manual part of the ingestion process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Gets Recorded
&lt;/h2&gt;

&lt;p&gt;Three tools get called through a real agent run: one declared and cleared for automatic execution, one declared but requiring approval, and one never declared at all.&lt;/p&gt;

&lt;p&gt;Running the agent against a task that touches all three produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;added column loyalty_tier (text) to customers
block: Tool 'table_drop' is explicitly blocked in this frame.
escalate: Tool 'schema_rename_table' is not in the allowed-tools list and requires manual review.
Finished the requested schema maintenance.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three show the Control Plane enforcing the manifest's guardrails. When the agent attempts to add a column, the Control Plane approves the action, since it's declared and deliberately allowed. When the agent attempts to drop the table, the Control Plane stops the action as blocked, since it's declared but requires human approval. When the agent tries &lt;code&gt;schema_rename_table&lt;/code&gt;, the action is stopped too, but as an escalation rather than a block: nobody declared it at all, so the Control Plane's default posture kicks in and sends it for human review instead of assuming it's safe. With a human in the loop, a decision can be made on the table renaming. The manifest can be edited to allow or block the agent's access to &lt;code&gt;schema_rename_table&lt;/code&gt;, ensuring clean future runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-Run Analysis
&lt;/h2&gt;

&lt;p&gt;After the run completes, the exported run record shows the difference between the Control-Plane-backed guard and the original exception-raising guard. A run record isn't a log someone has to remember to add or go hunting for later. Every LangChain agent built on this middleware produces a run record automatically, on every run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;actions&lt;/span&gt;   &lt;span class="err"&gt;3&lt;/span&gt;
&lt;span class="err"&gt;decisions&lt;/span&gt; &lt;span class="err"&gt;3&lt;/span&gt;
&lt;span class="err"&gt;blocked&lt;/span&gt;   &lt;span class="err"&gt;1&lt;/span&gt;
&lt;span class="err"&gt;reliance&lt;/span&gt;  &lt;span class="err"&gt;1&lt;/span&gt;
&lt;span class="err"&gt;bundle&lt;/span&gt;    &lt;span class="err"&gt;f97c5f38-01bf-4ce4-8934-edc5a86d4abe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blocked record itself is structured and timestamped, not a caught exception:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blocked_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"96c0ab4c-ca8a-42fc-b7bf-c8ed6d87dc8e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fa10af91-b3eb-4892-a7bf-a09eabfe29a6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"57604579-477d-43f0-a612-7b1f393ceb8f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tool 'table_drop' is explicitly blocked in this frame."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policy_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blocked_tool_policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blocked_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-11T02:58:45.594329+00:00"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six months from now, the answer to "did the agent ever try to drop a table" isn't a memory or a grep through application logs. It's this record, sitting in an exported run next to the one action that actually executed.&lt;/p&gt;

&lt;p&gt;Every LangChain agent that calls tools faces the same choice this post started with: enforce nothing and hope, or wire in guardrails before the agent gets access. Skip that choice, and the risk isn't abstract. It's a production database gone the way Replit's did, and a "my agent destroyed my SaaS" story of your own.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/cogno-us/cognous-agent-action-manifest" rel="noopener noreferrer"&gt;Agent Action Manifest&lt;/a&gt; and the &lt;a href="https://github.com/cogno-us/cognous-agent-control-plane" rel="noopener noreferrer"&gt;Agent Control Plane&lt;/a&gt; are two of the four layers Cognous built so that choice doesn't have to be made from scratch. They're open source, and the &lt;a href="https://github.com/cogno-us/cognous-open-control-stack" rel="noopener noreferrer"&gt;Open Control Stack repo&lt;/a&gt; has both, with examples to build an integration like this one from. Cloning the repo gives a LangChain agent solid guardrails and a record of every decision the agent makes.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>langchain</category>
      <category>security</category>
    </item>
    <item>
      <title>Agentic Guardrails for LangChain: The Manifest You Didn't Know You Needed</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:04:25 +0000</pubDate>
      <link>https://dev.to/cognous/agentic-guardrails-for-langchain-the-manifest-you-didnt-know-you-needed-3b28</link>
      <guid>https://dev.to/cognous/agentic-guardrails-for-langchain-the-manifest-you-didnt-know-you-needed-3b28</guid>
      <description>&lt;h2&gt;
  
  
  Guardrails, or the lack of them
&lt;/h2&gt;

&lt;p&gt;In July 2025, a Replit AI coding agent &lt;a href="https://www.theregister.com/2025/07/21/replit_saastr_vibe_coding_incident/" rel="noopener noreferrer"&gt;deleted a production database&lt;/a&gt; during a live coding session — despite explicit instructions not to touch production. It wasn't a hack or an outage. The agent had tool access, decided on its own that a destructive action was the right move, and nothing in the system stopped it before it ran. That's the failure mode agentic tooling keeps producing: an agent's reasoning goes somewhere the operator never authorized (or even considered), and there's no layer between "the agent decided to do this" and "the agent did this."&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://cogno.us" rel="noopener noreferrer"&gt;Open Control Stack&lt;/a&gt; is Cognous's answer to that gap — an &lt;a href="https://github.com/cogno-us" rel="noopener noreferrer"&gt;open-source framework&lt;/a&gt; that sits between an agent's decision to act and the tool actually executing, enforcing exactly what the agent is and isn't allowed to do.&lt;/p&gt;

&lt;p&gt;If your agents run on LangChain, the obvious question is how that enforcement actually reaches them. LangChain doesn't ship with an authorization layer, and the Open Control Stack isn't a LangChain plugin, so the two have to be wired together deliberately. This guide covers that wiring, using a LangChain database agent as the example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LangChain gets to decide
&lt;/h2&gt;

&lt;p&gt;LangChain's tool-calling agents are built around a simple loop: give the model a set of tools with names, descriptions, and argument schemas, and let the model decide — turn by turn — which tool to call and what arguments to pass. &lt;code&gt;bind_tools&lt;/code&gt; is how a tool gets attached to the model in the first place; &lt;code&gt;create_agent&lt;/code&gt; is what wires that model, its tools, and the call-and-respond loop between them into a runnable agent. These tools have become the default because they're model-agnostic — the same tool definitions work across the most popular LLMs: OpenAI, Anthropic, and other providers. And they turn "the agent should be able to look things up and take action" into a few lines of code instead of a hand-rolled parser.&lt;/p&gt;

&lt;p&gt;That convenience is also the gap. Once a tool is bound to the agent, there's no further policing. Say the agent has write access to a database. LangChain doesn't know the difference between the "helpful work" of adding columns and reindexing data, or the "unhelpful work" of the agent deciding, mid-task, that dropping the customer table is the best solution. As far as the framework is concerned, the call matches the tool's schema, and that's the whole contract. When LangChain holds the reins, it holds them exactly as far as "did I call a function that exists with arguments that match its schema" — and no further.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Open Control Stack fits
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97"&gt;Agent Action Manifest&lt;/a&gt; is the first of Cognous' Open Control Stack's four layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Declare&lt;/strong&gt; — what the agent is allowed to propose, before it runs (the Agent Action Manifest)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control&lt;/strong&gt; — what actually happens at runtime when the agent proposes an action (the &lt;a href="https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9"&gt;Agent Control Plane&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay&lt;/strong&gt; — a record of what was proposed, decided, and executed, packaged for later inspection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt; — that record compiled into something an auditor or compliance reviewer can actually read&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide stays in the Declare layer, building a manifest for the database agent from the example above: one declared action, and everything else left undeclared on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declaring the manifest
&lt;/h2&gt;

&lt;p&gt;The Agent Action Manifest is a JSON document that declares, outside of the agent's own code or prompt, what the agent is allowed to do: which tools it can call, what each specific action requires, and what happens by default when it tries something nobody wrote down. It's tool-agnostic: what's written for LangChain here will work for any other framework.&lt;/p&gt;

&lt;p&gt;The reason to keep this separate from the agent's own code is auditability: a reviewer can read the manifest and know exactly what an agent is permitted to do without tracing through prompts, tool definitions, and application logic to reconstruct the answer. It's also why the manifest gets declared and validated on its own, before any LangChain code touches it — the two aren't defined together, so nothing about the agent's permissions depends on how its orchestration framework happens to be wired.&lt;/p&gt;

&lt;p&gt;A manifest action needs, at minimum, a name, the tool it belongs to, and a &lt;code&gt;review_requirement&lt;/code&gt;. Here's the add-column action — safe, so it runs with no review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"schema_add_column"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db_tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add a nullable column to an existing table."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authority_required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db.schema.write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Permission to modify the application database schema."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data-platform-team"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"review_requirement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload_policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required_fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"column"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"column_type"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"optional_fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"default_value"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"forbidden_fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what about the stuff nobody can even imagine the agent might try — the things that could potentially destroy the company? There's no need to enumerate all the bad things, like &lt;code&gt;table_drop&lt;/code&gt;. The manifest sets a default posture for everything not explicitly defined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"escalate"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything the agent tries that isn't declared falls back to that posture. If the escalation reveals actions that need to be enumerated in the manifest, they get added. But this defensive posture prevents agents from creating catastrophic events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating the manifest
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aam&lt;/code&gt; is the Agent Action Manifest CLI — it checks a manifest file against the schema before anything downstream trusts it. Running &lt;code&gt;aam validate&lt;/code&gt; against the full file confirms it's well-formed before any LangChain code touches it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aam validate data_pipeline_agent.manifest.json
Validation result: VALID
Manifest ID:       data-pipeline-agent-v1
No issues found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that in place, the question becomes how to get LangChain to actually consult it instead of just checking that a function call matches its schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the manifest gives you
&lt;/h2&gt;

&lt;p&gt;A manifest action carries a couple of fields that matter for enforcement — &lt;code&gt;payload_policy.required_fields&lt;/code&gt; and &lt;code&gt;review_requirement.mode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The piece that's missing is a guard: a wrapper placed around each tool call that checks the manifest before the tool's own code runs. A guard checks three things before the underlying function executes — is the action declared at all, does the call have what &lt;code&gt;payload_policy&lt;/code&gt; requires, and does &lt;code&gt;review_requirement&lt;/code&gt; allow it to run immediately. Everything else in the manifest (authority scopes, redaction hints, reliance requirements) belongs to later stages in the stack — this integration only needs those three to decide allow, block, or escalate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing you add
&lt;/h2&gt;

&lt;p&gt;A decorator that sits between the LangChain &lt;code&gt;@tool&lt;/code&gt; wrapper and the function body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_action_manifest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_manifest&lt;/span&gt;

&lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_manifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_pipeline_agent.manifest.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;actions_by_tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;actions_by_tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ManifestBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Check a manifest action before the wrapped tool function runs.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;actions_by_tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;action_name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nd"&gt;@functools.wraps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ManifestBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; isn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t declared in the manifest — &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default posture is &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;

            &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload_policy&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required_fields&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ManifestBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: missing required fields &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;review_requirement&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ManifestBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: requires &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; before it can execute &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrapped&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decorator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it to a tool the same way you'd apply any other decorator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="nd"&gt;@guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db_tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema_add_column&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schema_add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;column_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Add a nullable column to a table.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;added column &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;column_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="nd"&gt;@guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db_tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table_drop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;table_drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Drop a table from the database.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dropped &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;Two calls: one declared and safe, one that was never written into the manifest at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;schema_add_column&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;column&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loyalty_tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;column_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;# "added column loyalty_tier (text) to customers"
&lt;/span&gt;
&lt;span class="n"&gt;table_drop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;# ManifestBlock: db_tool.table_drop isn't declared in the manifest —
# default posture is 'escalate'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;schema_add_column&lt;/code&gt; runs because it's declared with &lt;code&gt;review_requirement.mode: none&lt;/code&gt;. &lt;code&gt;table_drop&lt;/code&gt; never appears in the manifest — there's no entry to check &lt;code&gt;review_requirement&lt;/code&gt; on — so the guard falls back to &lt;code&gt;manifest.default_action&lt;/code&gt; and blocks it. Nobody had to anticipate &lt;code&gt;table_drop&lt;/code&gt; specifically. It's blocked by omission, which is the only kind of coverage that scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't do
&lt;/h2&gt;

&lt;p&gt;This guard blocks or allows synchronously, in-process. It doesn't route a blocked call to an actual approval queue, and it doesn't record anything for later review — that's the Agent Control Plane's job, not the manifest's.&lt;/p&gt;

&lt;p&gt;Extending the guard to hand decisions off to the Control Plane instead of just raising an exception is a natural next step — one that turns each allow or block into a recorded, traced, replayable decision instead of a one-off exception. That's its own guide, and will be covered in an upcoming post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this guide covered
&lt;/h2&gt;

&lt;p&gt;This guide wired the Agent Action Manifest into a LangChain agent's tool-calling layer to give it an allowlist of what it's actually permitted to do. That's what stops an agent from going off script and "helping" by dropping a table — the manifest blocks the call and escalates to a human before anything irreversible happens.&lt;/p&gt;

&lt;p&gt;The manifest and the Agent Action Manifest CLI can be found on &lt;a href="https://github.com/cogno-us/cognous-agent-action-manifest" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. More on the Open Control Stack, and how it functions as agentic guardrails, can be found at &lt;a href="https://cogno.us" rel="noopener noreferrer"&gt;cogno.us&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>The Evidence Pack That Answers "Can You Prove It?"</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Thu, 03 Sep 2026 19:54:13 +0000</pubDate>
      <link>https://dev.to/cognous/the-evidence-pack-that-answers-can-you-prove-it-1dld</link>
      <guid>https://dev.to/cognous/the-evidence-pack-that-answers-can-you-prove-it-1dld</guid>
      <description>&lt;p&gt;An agent that can read your systems and act on them needs guardrails, the same way a new hire with production access needs an onboarding checklist and someone reviewing their first few changes. That's what Cognous's &lt;a href="https://dev.to/cognous/cognous-keeps-your-ai-in-check-69l"&gt;Open Control Stack&lt;/a&gt; is: a &lt;a href="https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97"&gt;manifest&lt;/a&gt; declares what an agent may do before it ever runs, a &lt;a href="https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9"&gt;control plane&lt;/a&gt; enforces that live, and every run leaves behind a &lt;a href="https://dev.to/cognous/the-replay-bundle-that-remembers-what-happened-48l1"&gt;replay bundle&lt;/a&gt; — a signed file recording exactly what the agent proposed, what got decided, and what it relied on, so anyone can check it later.&lt;/p&gt;

&lt;p&gt;That's real technical proof, and it's the file you'd reach for in a deployment review — except the person running that review isn't going to open a JSON file and check whether a signature is valid. They're going to ask what the agent is for, what it can touch, what got blocked, and whether you can prove it, and they want that answer in a document, not a file they have to parse.&lt;/p&gt;

&lt;p&gt;That's the gap Agent Governance Evidence Pack closes. It doesn't record anything at runtime and it doesn't replace the replay bundle. It's where someone — a human, for now — turns the technical record into the document a risk committee actually reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Report Itself
&lt;/h2&gt;

&lt;p&gt;Cognous's tooling for this layer is &lt;code&gt;agep&lt;/code&gt; — a schema, a validator, and a renderer. Run &lt;code&gt;agep render&lt;/code&gt; on a completed pack and this is what comes out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;agep render support_agent_evidence_pack.json &lt;span class="nt"&gt;--out&lt;/span&gt; report.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## 1. Executive Summary&lt;/span&gt;

&lt;span class="gs"&gt;**SupportAgent**&lt;/span&gt; is deployed in the &lt;span class="gs"&gt;**production**&lt;/span&gt; environment. Business
purpose: Reduce first-response time on support tickets by pulling account
context and drafting a reply for a human to send.

The agent has access to &lt;span class="gs"&gt;**3**&lt;/span&gt; tool(s) and can propose &lt;span class="gs"&gt;**3**&lt;/span&gt; action(s), of
which &lt;span class="gs"&gt;**2**&lt;/span&gt; require authority or human review.

&lt;span class="gs"&gt;**1**&lt;/span&gt; replay bundle(s) are available (1 signed).
Validation summary: 1 valid bundle(s), 0 invalid, 0 error(s), 1 warning(s).

&lt;span class="gs"&gt;**1**&lt;/span&gt; open risk(s) (0 critical).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the report breaks into eight areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent and Deployment Overview&lt;/strong&gt; — who owns the agent, why it exists, and where it's running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool and Action Inventory&lt;/strong&gt; — every tool the agent can reach and every action it can propose, each flagged for whether it needs authority or review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authority Model&lt;/strong&gt; — the rules of authority itself: what counts as privileged, how a grant gets requested, whether it expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy Controls&lt;/strong&gt; — the mechanisms actually implemented to enforce those rules, each with a status and an evidence reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocked-Action and Reliance Summaries&lt;/strong&gt; — what got stopped and why, and what data the agent leaned on to do its job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay Bundle Inventory and Validation Summary&lt;/strong&gt; — which replay bundles back this report up, and whether those bundles passed validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redaction and Export Summary&lt;/strong&gt; — a record that the replay bundle was redacted for an outside recipient: which fields were stripped and where the redacted file lives, not the redacted content itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk Register&lt;/strong&gt; — open risks, each tied back to real evidence rather than asserted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two sections are left off that list on purpose. Review Records is empty because this pack is still &lt;code&gt;in_review&lt;/code&gt; — a review record only exists once someone's actually signed off, and nobody has yet. Known Limitations is fixed text the renderer appends to every pack regardless of content, not something specific to this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing Builds This For You
&lt;/h2&gt;

&lt;p&gt;The evidence pack is built from a JSON file. That file is data-driven, but human-curated: it incorporates data pulled from the replay bundle — the block count, the reliance record, and so on.&lt;/p&gt;

&lt;p&gt;Pulling those raw facts out of the bundle takes a script written for this workflow. It's not part of the evidence pack format itself.&lt;/p&gt;

&lt;p&gt;As an example, &lt;code&gt;send_email&lt;/code&gt; got blocked once. The script counts that, and pulls the raw reason logged for it: &lt;code&gt;Tool 'send_email' is explicitly blocked in this frame.&lt;/code&gt; That's as far as the script gets. The schema doesn't leave room to add anything more structured than that — a blocked-action record has exactly one free-text field for explaining what happened, &lt;code&gt;reason_summary&lt;/code&gt;, and nothing else. So if you also want the record to say &lt;em&gt;why&lt;/em&gt; nothing overrode the block — that no authority was ever granted for it — the only place that fact can go is into that same sentence, written by a person.&lt;/p&gt;

&lt;p&gt;Here's what that produced, with each field marked for where it came from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send_customer_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"external_send"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason_summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Email send blocked: send_email is explicitly blocked in this frame and no authority grant for the email.send.customer scope exists."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence_reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run:e6d9a08e-40cf-4b4f-b4dd-82201bc15901"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tool_name&lt;/code&gt;, &lt;code&gt;action_type&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, and &lt;code&gt;evidence_reference&lt;/code&gt; all come straight from the bundle. &lt;code&gt;action_name&lt;/code&gt; is a readable label a person chose — the bundle only knows the tool as &lt;code&gt;send_email&lt;/code&gt;. &lt;code&gt;reason_summary&lt;/code&gt; is the one sentence written from scratch.&lt;/p&gt;

&lt;p&gt;The same script could call an LLM to write &lt;code&gt;reason_summary&lt;/code&gt; instead of a person — combining a couple of facts into one sentence is exactly what an LLM is good at. Cognous has a replay-bundle import helper and "richer executive summaries — enhanced narrative generation" on the roadmap, so a script-plus-LLM setup is a reasonable stopgap until that lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation Catches What You Forget
&lt;/h2&gt;

&lt;p&gt;Once a pack is built, &lt;code&gt;agep validate&lt;/code&gt; checks it against the schema's own rules — things like "if an action requires authority, the pack has to say what that authority actually is." The first draft of this pack didn't say that, and validation caught it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;agep validate support_agent_evidence_pack.json
Pack ID : ep-support-agent-2026-001
Valid   : False
Issues  : 4
  &lt;span class="o"&gt;[&lt;/span&gt;ERROR] E018 &lt;span class="o"&gt;[&lt;/span&gt;authority_model]: One or more actions have &lt;span class="nv"&gt;authority_required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;True but authority_model is missing.
  &lt;span class="o"&gt;[&lt;/span&gt;WARNING] W014 &lt;span class="o"&gt;[&lt;/span&gt;review_records]: review_records is empty.
  &lt;span class="o"&gt;[&lt;/span&gt;WARNING] W019 &lt;span class="o"&gt;[&lt;/span&gt;action_inventory]: Action &lt;span class="s1"&gt;'draft_ticket_reply'&lt;/span&gt; is &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'write'&lt;/span&gt; but authority_required is False.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error is straightforward: &lt;code&gt;send_customer_email&lt;/code&gt; was marked as needing authority, but nothing in the pack said what that authority was. That's not something the script would catch — it only reads replay bundles, and a replay bundle doesn't carry that information. It exists, though: the manifest declares it, back at the Declare layer, as the &lt;code&gt;email.send.customer&lt;/code&gt; scope. The script just never looks there. The fix is adding it by hand: the &lt;code&gt;email.send.customer&lt;/code&gt; scope from the manifest, with a note that no grant exists yet for this deployment. That note is accurate. It's the real reason the send got blocked instead of just flagged for review.&lt;/p&gt;

&lt;p&gt;W019 is worth a one-line note: the validator's docs undersell which action types trigger it — the code also flags plain &lt;code&gt;write&lt;/code&gt; actions like &lt;code&gt;draft_ticket_reply&lt;/code&gt;, not just external sends. The warning is left standing rather than papered over, because &lt;code&gt;draft_reply&lt;/code&gt; genuinely has no authority gate today.&lt;/p&gt;

&lt;p&gt;With the missing authority section added, the pack validates clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;agep validate support_agent_evidence_pack.json
Valid   : True
Issues  : 3
  &lt;span class="o"&gt;[&lt;/span&gt;WARNING] W014 &lt;span class="o"&gt;[&lt;/span&gt;review_records]: review_records is empty.
  &lt;span class="o"&gt;[&lt;/span&gt;WARNING] W015 &lt;span class="o"&gt;[&lt;/span&gt;review_status]: Production deployment has review_status &lt;span class="s1"&gt;'draft'&lt;/span&gt; or &lt;span class="s1"&gt;'in_review'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;WARNING] W019 &lt;span class="o"&gt;[&lt;/span&gt;action_inventory]: Action &lt;span class="s1"&gt;'draft_ticket_reply'&lt;/span&gt; is &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'write'&lt;/span&gt; but authority_required is False.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three remaining warnings are honest, not bugs to fix. There's no review record because nobody has reviewed this pilot run yet. &lt;code&gt;draft_reply&lt;/code&gt; genuinely has no authority gate. A pack with zero warnings at this stage would be hiding something, not proving something.&lt;/p&gt;

&lt;p&gt;That gap is also the pack's one open risk. The Risk Register says the same thing in plain terms: &lt;code&gt;draft_reply&lt;/code&gt; needs manual review on every single invocation, because it was never added to the control plane's allowed-tools list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirming the Evidence
&lt;/h2&gt;

&lt;p&gt;The last step is a manual approval. Recall that the &lt;code&gt;Review Records&lt;/code&gt; section sits empty right now. Once validation passes, a human — typically a CISO or Risk Manager — reviews the pack and signs off, adding a &lt;code&gt;ReviewRecord&lt;/code&gt;: their name, their role, a decision, and a timestamp. That's what moves &lt;code&gt;review_status&lt;/code&gt; from &lt;code&gt;in_review&lt;/code&gt; to &lt;code&gt;approved&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The schema enforces this, not just documents it. A pack can't be marked &lt;code&gt;approved&lt;/code&gt; without that record on file, and a critical, unresolved risk in the register blocks approval outright, no matter who signed off.&lt;/p&gt;

&lt;p&gt;With the official sign-off, the evidence pack is ready for whatever comes next — an audit, a board review, or wider deployment of the agent it covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving The Whole Stack Held
&lt;/h2&gt;

&lt;p&gt;In this series, we've covered the four layers of Cognous's &lt;a href="https://dev.to/cognous/cognous-keeps-your-ai-in-check-69l"&gt;Open Control Stack&lt;/a&gt;: a &lt;a href="https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97"&gt;manifest&lt;/a&gt; that declares what an agent may do, a &lt;a href="https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9"&gt;control plane&lt;/a&gt; that enforces it live, a &lt;a href="https://dev.to/cognous/the-replay-bundle-that-remembers-what-happened-48l1"&gt;replay bundle&lt;/a&gt; that proves a specific run followed the rules, and an evidence pack that turns all of that into something a non-engineer can actually approve or reject. Each layer produces something real the next one consumes — sometimes with a clean mapping, sometimes, like this one, with a script and a person doing the work no importer does yet.&lt;/p&gt;

&lt;p&gt;Clone &lt;a href="https://github.com/cogno-us/cognous-agent-governance-evidence-pack" rel="noopener noreferrer"&gt;Agent Governance Evidence Pack&lt;/a&gt; and try &lt;code&gt;agep&lt;/code&gt; against the example packs in the repo, or build one from your own control plane and replay bundle records.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Replay Bundle That Remembers What Happened</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Tue, 25 Aug 2026 12:58:42 +0000</pubDate>
      <link>https://dev.to/cognous/the-replay-bundle-that-remembers-what-happened-48l1</link>
      <guid>https://dev.to/cognous/the-replay-bundle-that-remembers-what-happened-48l1</guid>
      <description>&lt;p&gt;It's 2am. A customer complains about something an agent did. The next morning the CEO is already asking the only question that matters: did the agent do something it shouldn't have?&lt;/p&gt;

&lt;p&gt;"I'm pretty sure our guardrails are solid" isn't an answer anyone wants to give the boss. It certainly won't fly with a customer, an auditor, or with legal. What they want is proof, and proof means being able to answer specifically: what did the agent try to do, on this exact run, and what happened when it tried?&lt;/p&gt;

&lt;p&gt;If that answer lives in the timestamps of a few hundred servers and a half-dozen tool integrations, you don't have proof. You have a scavenger hunt. "Trust us, it's fine" does not survive a scavenger hunt.&lt;/p&gt;

&lt;p&gt;Cognous' Open Control Stack exists to close that gap. It follows four steps: Declare → Control → Replay → Evidence.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Declare:&lt;/strong&gt; Agents need guardrails to tell them what they can and cannot do — that's a &lt;a href="https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97"&gt;manifest&lt;/a&gt;, declared up front.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control:&lt;/strong&gt; They need something enforcing those guardrails live — that's a &lt;a href="https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9"&gt;control plane&lt;/a&gt;, deciding allow/block/escalate on every proposal in real time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, we'll cover Replay: no longer defining and patrolling the guardrails, but the receipts that the rails held. Taking what the Control Plane recorded during a run and turning it into one portable file — the thing you actually hand someone instead of a text file with pasted logs from a dozen systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What The Control Plane Hands Off
&lt;/h2&gt;

&lt;p&gt;Picture a workflow mid-run: actions getting proposed, the gate deciding each one, live.&lt;/p&gt;

&lt;p&gt;Now what? The Control Plane made the right calls in the moment — but how do you actually know that, after the fact, without having watched it happen live? That's the question Replay answers: it takes what already happened and turns it into something you can go back and inspect.&lt;/p&gt;

&lt;p&gt;The Control Plane keeps a recorder running for the life of a run — that's what's actually holding the frame, the proposals, the decisions, the traces as they happen. At the end of the run, the recorder exports everything it captured as a replay bundle: one file you can store, ship, or hand off for later analysis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_replay_bundle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_bundle.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run_bundle.json&lt;/code&gt; is the full playback of the workflow — every decision, every trace, and the evidence behind the one blocked action, &lt;code&gt;send_email&lt;/code&gt;, all in one place. It's exactly what you'd look at to understand an error, or hand over if there's a deeper investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Replay Tool
&lt;/h3&gt;

&lt;p&gt;Agent Replay Bundle ships its own CLI, &lt;code&gt;arb&lt;/code&gt;, for working with files like this one — validating, summarizing, redacting, signing. Start with validation, pointed at the bundle the Control Plane just exported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;arb validate run_bundle.json
Error: Replay bundle failed model validation: 1 validation error &lt;span class="k"&gt;for &lt;/span&gt;AgentReplayBundle
bundle_id
  Field required &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;missing]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fails immediately. The Control Plane's export calls the field &lt;code&gt;replay_bundle_id&lt;/code&gt;; &lt;code&gt;arb&lt;/code&gt;'s schema — the &lt;code&gt;AgentReplayBundle&lt;/code&gt; model that &lt;code&gt;validate&lt;/code&gt; checks against — calls it &lt;code&gt;bundle_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Agent Replay Bundle is a public interchange format, meant for any system, and Agent Control Plane's internal run record shapes several of the schema terms differently than the Replay Bundle. Running validation means performing a mapping step between the two first — small, mostly renames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bundle_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replay_bundle_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frame&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frame&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action_proposals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_decisions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decisions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_traces&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_traces&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authority_records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authority_records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reliance_records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reliance_records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked_actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked_actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that, and the same run record now validates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;arb validate mapped_replay_bundle.json
VALID  &lt;span class="nv"&gt;bundle_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ae564206-8daf-4b19-9d60-db54f56387a5  &lt;span class="nv"&gt;issues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
  WARNING W005: Bundle has no authority records.
  WARNING W012: signature_metadata is missing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Valid — with two warnings, not zero. Neither one blocks validity, and both are honest about the bundle's actual state: no authority was granted for this run (which is &lt;em&gt;why&lt;/em&gt; two of the three actions didn't just allow), and nothing's been signed yet.&lt;/p&gt;

&lt;p&gt;Worth building into your own pipeline: none of this runs automatically. &lt;code&gt;generate_replay_bundle()&lt;/code&gt; builds the object; &lt;code&gt;arb validate&lt;/code&gt; is a separate command a human has to think to run. Nothing stops a bundle from sitting unvalidated for months until the day someone actually needs it — which is the worst possible time to find out it doesn't pass. Wire the mapping and the validation into the end of every run, not into the moment an auditor asks for one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What A Reviewer Sees First
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;arb summarize&lt;/code&gt; is the one-screen version — what you'd look at before deciding whether to open the full bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;arb summarize mapped_replay_bundle.json
bundle_id:          ae564206-8daf-4b19-9d60-db54f56387a5
run_id:             c3f474dd-952b-4f27-ab5c-98de868b3c67
status:             &lt;span class="nb"&gt;complete
&lt;/span&gt;actor:              support-agent-v1
environment:        production
policy_version:     v1.0
action_proposals:   3
policy_decisions:   3
policy_traces:      3
blocked_actions:    1
authority_records:  0
reliance_records:   1
final_output:       present
redacted:           no
signed:             no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three proposals, three decisions, one block, zero authority records. That last number is the tell — it's the same fact the warning surfaced, now visible at a glance instead of buried in a rule name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redacting For Export
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mapped_replay_bundle.json&lt;/code&gt; — the bundle we just validated — still has raw targets and payloads in it: customer IDs, draft email bodies, potentially PII. Fine for an internal review, not fine to hand to outside counsel.&lt;/p&gt;

&lt;p&gt;Redaction isn't content-aware — it doesn't scan text and detect what looks sensitive. It replaces a fixed set of fields wholesale: &lt;code&gt;payload&lt;/code&gt; (redacted by default), plus &lt;code&gt;target&lt;/code&gt; and &lt;code&gt;final_output&lt;/code&gt; if you opt in with flags. Everything structural — IDs, timestamps, decision results, policy names — is preserved no matter what:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;arb redact mapped_replay_bundle.json &lt;span class="nt"&gt;--out&lt;/span&gt; redacted_bundle.json &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="nt"&gt;--final-output&lt;/span&gt;
Redacted bundle written to redacted_bundle.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"redaction_metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redacted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redacted_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-18T19:47:34Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redacted_fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"action_proposals[0].payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"action_proposals[0].target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"action_proposals[1].payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"action_proposals[1].target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"action_proposals[2].payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"action_proposals[2].target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"final_output"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"replacement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[REDACTED]"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A redacted bundle still validates clean — the whole point is that a reviewer can see &lt;em&gt;what&lt;/em&gt; was blocked and &lt;em&gt;why&lt;/em&gt;, without seeing the customer's actual email address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing For Export Integrity
&lt;/h2&gt;

&lt;p&gt;Once a bundle is ready to leave the building, sign it so anyone downstream can tell if it was altered in transit — the redacted version, since that's the one actually going out the door:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;arb sign redacted_bundle.json &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="s2"&gt;"demo-secret"&lt;/span&gt; &lt;span class="nt"&gt;--key-id&lt;/span&gt; &lt;span class="s2"&gt;"cognous-demo-key-01"&lt;/span&gt; &lt;span class="nt"&gt;--out&lt;/span&gt; signed_bundle.json
Signed bundle written to signed_bundle.json

&lt;span class="nv"&gt;$ &lt;/span&gt;arb verify signed_bundle.json &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="s2"&gt;"demo-secret"&lt;/span&gt;
Signature VALID

&lt;span class="nv"&gt;$ &lt;/span&gt;arb verify signed_bundle.json &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="s2"&gt;"wrong-secret"&lt;/span&gt;
Signature INVALID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's HMAC-SHA256 over a canonical serialization of the bundle. Worth being precise about what it proves: it tells you the bundle hasn't changed since it was signed, given the secret. It does not prove &lt;em&gt;who&lt;/em&gt; signed it — there's no identity binding, no PKI, no protection if the secret itself leaks. For production use, that's a job for a real key management system. For a shared-secret export check between two parties who already trust each other, HMAC does what it says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading The Verified File
&lt;/h2&gt;

&lt;p&gt;The auditor gets &lt;code&gt;signed_bundle.json&lt;/code&gt;, runs &lt;code&gt;verify&lt;/code&gt;, and knows immediately whether it matches what left your hands. Signing isn't encryption, though — &lt;code&gt;verify&lt;/code&gt; is the only command that speaks the signed wrapper (&lt;code&gt;signed_bundle_id&lt;/code&gt;, &lt;code&gt;replay_bundle&lt;/code&gt;, &lt;code&gt;signature_metadata&lt;/code&gt;). To actually read the run, pull the inner bundle back out first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signed_bundle.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;signed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replay_bundle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;extracted.json&lt;/code&gt; is a plain &lt;code&gt;AgentReplayBundle&lt;/code&gt; again — &lt;code&gt;summarize&lt;/code&gt;, &lt;code&gt;validate&lt;/code&gt;, everything else works on it. Verify first, extract second: confirm nothing's been tampered with before you trust a single field inside it.&lt;/p&gt;

&lt;p&gt;The signature also doesn't prove anything about the gap between when the run happened and when you got around to signing it. &lt;code&gt;signed_at&lt;/code&gt; is just whatever time you ran the &lt;code&gt;sign&lt;/code&gt; command — there's no check against &lt;code&gt;generated_at&lt;/code&gt;. A bundle that sits unsigned for three days is three days an editor could have touched the file with no trace of it; sign it on day three, and &lt;code&gt;verify&lt;/code&gt; comes back clean regardless. The signature only covers the window it's actually been alive for. So the real answer isn't "sign it before you send it" — it's validate, redact, and sign as part of the run itself, the moment the bundle is generated, so there's no gap for anything to slip into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving The Guardrails Held
&lt;/h2&gt;

&lt;p&gt;Back to that morning-after meeting: the complaint, and the question from the boss. What was once a stressful scavenger hunt across servers matching timestamps and logfiles is now just opening one file.&lt;/p&gt;

&lt;p&gt;Agentic runs get their guardrails from two layers: a manifest declaring what's allowed, and a control plane enforcing that live. Replay is the layer that lets your team prove those guardrails actually held — turning what the control plane recorded into one validated, signed file anyone can check independently, long after the run itself is over.&lt;/p&gt;

&lt;p&gt;Clone &lt;a href="https://github.com/cogno-us/cognous-agent-replay-bundle" rel="noopener noreferrer"&gt;Agent Replay Bundle&lt;/a&gt; and try &lt;code&gt;arb&lt;/code&gt; against the example bundles in the repo, or your own control plane exports.&lt;/p&gt;

&lt;p&gt;Learn more about Agent Replay Bundle at &lt;a href="https://cogno.us/agent_replay_bundle.html" rel="noopener noreferrer"&gt;cogno.us&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>Cognous Control Plane: The Layer That Tells Your Agent No</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Tue, 18 Aug 2026 19:39:36 +0000</pubDate>
      <link>https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9</link>
      <guid>https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9</guid>
      <description>&lt;p&gt;It's all too common — a viral tweet describing how AI agents with poorly defined guardrails deleted production, wiped a critical database, or caused some other disastrous occurrence. Agents need guardrails: this includes permissions to do the job, but also blockers that limit the reach of the agent, thereby preventing accidental disasters that fuel viral tweets.&lt;/p&gt;

&lt;p&gt;Cognous' Open Control Stack is a framework for wrapping AI agents in guardrails, providing access where needed, but blocking the boundaries that cannot be crossed.&lt;/p&gt;

&lt;p&gt;In our &lt;a href="https://dev.to/cognous/cognous-keeps-your-ai-in-check-69l"&gt;introductory blog post&lt;/a&gt;, our agent drafted a cringey follow-up email and "helpfully" sent it to a company's entire list of top customers. Nobody approved that send, and the human marketing team had to drop everything and work to mitigate the damage.&lt;/p&gt;

&lt;p&gt;Open Control Stack is structured as four layers: &lt;strong&gt;Declare → Control → Replay → Evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We built the first layer in &lt;a href="https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97"&gt;The Manifest That Keeps Your AI Agent Honest&lt;/a&gt;, the &lt;strong&gt;Agent Action Manifest&lt;/strong&gt;. That's the Declare layer — a JSON file that works as the allow/block list for our agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pull the top customer:&lt;/strong&gt; allowed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft a reply:&lt;/strong&gt; draft-first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull contract details:&lt;/strong&gt; blocked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send the email:&lt;/strong&gt; needs approval&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But that's all a manifest is — a list. Nothing reads it and nothing enforces it. We need something that takes that list and actually applies it to the agent while it's running.&lt;/p&gt;

&lt;p&gt;That's the &lt;strong&gt;Agent Control Plane&lt;/strong&gt; — the Control layer. This post is about what it actually does when the agent tries something.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Sits Beside The Agent
&lt;/h2&gt;

&lt;p&gt;The Control Plane doesn't run the agent, and it isn't a framework. It sits next to whatever is already generating the agent's behavior — OpenClaw, Hermes, a custom loop, whatever — and turns every proposed action into a recorded decision before that action is treated as real.&lt;/p&gt;

&lt;p&gt;The agent's job is small in this picture — it proposes. The Control Plane does the rest: it decides, and it documents. Propose is the only row the agent touches. Decide is the gate ruling allow, block, or escalate. Document is what gets written down once the decision's made. The agent doesn't get a say in the outcome — it just gets to ask.&lt;/p&gt;

&lt;p&gt;The pattern, end to end:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjv1xnlduxc8db6j7pjl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjv1xnlduxc8db6j7pjl.png" alt="Cognous Control Plane Pattern" width="799" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Propose
&lt;/h3&gt;

&lt;p&gt;The agent wants to do something. The Control Plane frames it, and a proposal gets built.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent Task:&lt;/strong&gt; The agent kicks off the run. In our use case: "review a customer account and follow up."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frame:&lt;/strong&gt; The Control Plane frames the execution context for the run — the task, actor, environment, allowed tools, blocked tools, policy version. This gets locked in at the start and becomes immutable for the life of the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action Proposal:&lt;/strong&gt; The agent's ask — this tool, this action type, this target, this payload, this reason. Recorded before anything executes — that's the point of the proposal steps. If the decision gate blocks it next, nothing downstream ever sees a real tool call, but the attempt is still on the record.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Decide
&lt;/h3&gt;

&lt;p&gt;The Control Plane decides whether that proposal is actually okay. This is where the manifest's declarations actually take effect: what it declared is what ends up in the frame's allowed and blocked lists, and the frame is what the gate checks against. Two of the six rules below check something separate from the frame: authority — whether this actor currently has permission to write, or to send things externally. Authority is granted per run, on top of the frame, and it's what rules 3 and 5 are checking for.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Policy Gate:&lt;/strong&gt; Decides what happens to the proposal. Six rules, checked top to bottom, first match wins:

&lt;ol&gt;
&lt;li&gt;tool is explicitly blocked → &lt;code&gt;block&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;tool is not in the allowed list → &lt;code&gt;escalate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;action type is &lt;code&gt;external_send&lt;/code&gt; → &lt;code&gt;allow&lt;/code&gt; if authority for it exists, otherwise &lt;code&gt;block&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;action type is &lt;code&gt;read&lt;/code&gt; (and passed rules 1–2) → &lt;code&gt;allow&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;action type is &lt;code&gt;write&lt;/code&gt; → &lt;code&gt;allow&lt;/code&gt; if authority for it exists, otherwise &lt;code&gt;escalate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;anything else → &lt;code&gt;escalate&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy Decision + Evaluation Trace:&lt;/strong&gt; The gate's verdict, plus the rule-by-rule path it took to reach it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow / Block / Escalate:&lt;/strong&gt; The three possible verdicts. Only &lt;code&gt;allow&lt;/code&gt; lets the proposal go on to actually touch a tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Document
&lt;/h3&gt;

&lt;p&gt;Once a decision is made, the Control Plane writes down what happened, what the decision was, and how it was made.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reliance Record:&lt;/strong&gt; Once an action is allowed and actually executed, a record of what the agent depended on to produce its output — a tool, a database, a file, an API, user input. Blocked and escalated actions never execute, so there's nothing to record reliance on — dependence doesn't exist until an action actually runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run Record:&lt;/strong&gt; Every proposal, decision, trace, block, and reliance from this run, assembled into one object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay Bundle:&lt;/strong&gt; The run record packaged up for later, portable inspection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running The Scenario For Real
&lt;/h2&gt;

&lt;p&gt;Let's take three of those actions from the manifest and actually run them through the gate: &lt;code&gt;pull_top_customers&lt;/code&gt;, &lt;code&gt;draft_reply&lt;/code&gt;, and &lt;code&gt;send_email&lt;/code&gt;. All three are on this run's allowed-tools list. &lt;code&gt;pull_contract_details&lt;/code&gt; is also declared in the frame's blocked-tools list, matching the manifest — but the task is "review a customer account and follow up," which never calls for contract details, so the agent never proposes it. It's declared, not exercised: nothing for the gate to evaluate here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting the run:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the Control Plane spinning up: &lt;code&gt;RunRecorder()&lt;/code&gt; creates the recorder instance, and &lt;code&gt;start_run()&lt;/code&gt; builds the frame — the same frame from the Propose section above, locking in the task, actor, environment, allowed tools, blocked tools, and policy version for everything that follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_control_plane&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RunRecorder&lt;/span&gt;

&lt;span class="n"&gt;recorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RunRecorder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review a customer account and follow up.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-agent-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allowed_tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pull_top_customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft_reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;send_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;blocked_tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pull_contract_details&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;policy_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Proposing and Deciding One Action:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the pattern for a single action, &lt;code&gt;pull_top_customers&lt;/code&gt;. The same process repeats for the other two actions — omitted here for brevity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Propose the action with the &lt;code&gt;propose_action&lt;/code&gt; method. This is stored in the variable &lt;code&gt;a1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Evaluate the proposal with &lt;code&gt;evaluate_action&lt;/code&gt;. This returns two values: the decision, and, if the action is blocked, a blocked-action record.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;a1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;propose_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pull_top_customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crm:top_customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Identify the customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s account tier before drafting a reply.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pull_top_customers&lt;/code&gt; is not blocked, so we receive only the decision, and the unused second value gets thrown away with &lt;code&gt;_&lt;/code&gt;. &lt;code&gt;d1&lt;/code&gt; holds the decision: the verdict (&lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;block&lt;/code&gt;, or &lt;code&gt;escalate&lt;/code&gt;), which rule produced it, and why. Here's what the gate decides across all three actions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Rule&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;pull_top_customers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft_reply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;write&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;escalate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;send_email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;external_send&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Missing authority is what stops two of the three from just allowing. &lt;code&gt;draft_reply&lt;/code&gt; escalates and &lt;code&gt;send_email&lt;/code&gt; blocks — same problem, nobody granted permission for this run — but different severity, because the two actions aren't equally reversible. A blocked send is stopped outright: once an email's out, it's out. An escalated write just doesn't execute yet. Nothing about the original decision ever changes — but if someone grants write authority before this run ends, a fresh proposal for the same action would clear rule 5 as &lt;code&gt;allow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The evaluation trace is where the reasoning survives, not just the verdict. Here's the trace for the &lt;code&gt;send_email&lt;/code&gt; decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules_evaluated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"rule_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blocked_tool_policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matched"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"rule_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unknown_tool_policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matched"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rule_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"external_send_authority_policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matched"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Action type 'external_send' requires an authority record with scope 'external_send', which was not found."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"final_result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reviewer looking at this trace can tell exactly which check failed and why — not just that the action was blocked. That matters because &lt;code&gt;send_email&lt;/code&gt; is on the &lt;em&gt;allowed&lt;/em&gt;-tools list; nothing about the tool itself was forbidden. The trace is what proves the block was about missing authority, not a banned tool.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Worth noting:&lt;/strong&gt; &lt;code&gt;draft_reply&lt;/code&gt; escalating isn't a lesser outcome than blocking. It's easy to read "escalate" as a downgrade of "block," but it's really the gate saying it doesn't have enough information to decide safely, so a human should. Grant &lt;code&gt;write&lt;/code&gt; authority to the run, and the same action clears rule 5 as &lt;code&gt;allow&lt;/code&gt; instead — the rule didn't change; what the actor is authorized to do did.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Documenting The Proposal And Decision
&lt;/h2&gt;

&lt;p&gt;After a decision has been made, the Cognous Control Plane documents what occurred during the process. All of the information is collated and stored in one place — not grabbed from logs scattered across different tools and pieced together by timestamp.&lt;/p&gt;

&lt;p&gt;The first step is the Reliance Record. Recall that this only documents actions that actually ran — in this case, &lt;code&gt;pull_top_customers&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_reliance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;source_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pull_top_customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer tier and account fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;referenced_action_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;a1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't get written to a file by itself — it's held in memory as part of the run, alongside every proposal, decision, and trace, until the whole thing gets closed out and exported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Draft prepared and escalated for review. Email send was blocked, not attempted.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_record.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_replay_bundle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;export_json()&lt;/code&gt; writes everything — proposals, decisions, traces, the one blocked-action record, that one reliance record — to a &lt;code&gt;RunRecord&lt;/code&gt; JSON file. Retrieving the reliance record later is just reading that file and looking at its &lt;code&gt;reliance_records&lt;/code&gt; array.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading The Results
&lt;/h2&gt;

&lt;p&gt;Writing the record is only half the story — you also need a way to check it. That's &lt;code&gt;acp&lt;/code&gt;, the Control Plane's own command-line tool, shipped in the same repo: &lt;a href="https://github.com/cogno-us/cognous-agent-control-plane" rel="noopener noreferrer"&gt;github.com/cogno-us/cognous-agent-control-plane&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Running it against the run record we just exported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acp validate-run run_record.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RunRecord 79ad6860-03de-4136-9828-d4e40fbd05f4: valid (0 errors, 0 warnings)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation here isn't checking whether the run succeeded — it's checking that the record is internally consistent: every decision references an action that actually exists, every blocked action corresponds to a decision that actually says &lt;code&gt;block&lt;/code&gt;. It's a check on the evidence, not on the outcome.&lt;/p&gt;

&lt;p&gt;The replay bundle gets the same treatment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acp validate-replay replay_bundle.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ReplayBundle 535c72f8-efee-4144-9b78-7f7fff73133d: valid (0 errors, 0 warnings)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What This Deliberately Doesn't Do
&lt;/h2&gt;

&lt;p&gt;The Control Plane only enforces the rules and authority it's actually been given — it doesn't infer intent, and it doesn't fill gaps with a best guess. A permission that was never granted shows up as an escalation or a block, not a pass. It's not a compliance system, and it's not a guarantee that the agent's output was correct. It's the layer that turns "the agent tried to do X" into a recorded, deterministic, inspectable decision, every time.&lt;/p&gt;

&lt;p&gt;It also doesn't decide what happens after a decision is made. The gate doesn't open a ticket, notify anyone, or track whether an escalation ever gets reviewed. Whether a human looks at the draft, whether the agent retries, whether authority eventually gets granted — that's handled by whatever's orchestrating the agent, not by the Control Plane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Up
&lt;/h2&gt;

&lt;p&gt;We now have a run: three proposed actions, one blocked, one escalated, one allowed, each with a trace and a fingerprint, packaged into a replay bundle that validates clean. That bundle is a record of one run.&lt;/p&gt;

&lt;p&gt;The Cognous Open Control Stack has four steps: Declare → Control → Replay → Evidence.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97"&gt;The Manifest That Keeps Your AI Agent Honest&lt;/a&gt;, we declared in the manifest what was allowed and what was not. In this post, we walked through the Control Plane — it runs next to your agentic framework to allow, escalate, or block actions. The Control Plane records the results from every action taken.&lt;/p&gt;

&lt;p&gt;Next, we look at the last two steps: Replay and Evidence. These are the tools that will save you when auditors are knocking at the door, or you need to prove you've redacted customer data, or just prove it hasn't been tampered with since it was generated. That's the &lt;strong&gt;Agent Replay Bundle&lt;/strong&gt;, and we'll cover it in our next post.&lt;/p&gt;

&lt;p&gt;In the meantime, the Control Plane repo, examples, schemas, and test suite are live now: &lt;a href="https://github.com/cogno-us/cognous-agent-control-plane" rel="noopener noreferrer"&gt;github.com/cogno-us/cognous-agent-control-plane&lt;/a&gt;. Clone it, run &lt;code&gt;examples/simple_agent_run.py&lt;/code&gt;, and watch the gate make the call.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>security</category>
    </item>
    <item>
      <title>The Manifest That Keeps Your AI Agent Honest</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Wed, 12 Aug 2026 14:50:04 +0000</pubDate>
      <link>https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97</link>
      <guid>https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97</guid>
      <description>&lt;p&gt;In our first post, &lt;a href="https://dev.to/cognous/cognous-keeps-your-ai-in-check-69l"&gt;Cognous Keeps Your AI in Check&lt;/a&gt;, we introduced the scenario: a customer-service agent that pulls CRM records, drafts replies, and — if nobody's watching — occasionally sends 1,000 tone-deaf emails to your best clients. We said the fix isn't reading what the agent wrote after the fact. It's deciding, in advance, what the agent is even allowed to attempt.&lt;/p&gt;

&lt;p&gt;That decision has to live somewhere. It can't live in a Slack thread, a comment in the agent's system prompt, or "Dave from platform remembers we blocked that." It needs to be a file — versioned, reviewable, and boring enough that a security team can actually sign off on it.&lt;/p&gt;

&lt;p&gt;That's what Cognous's Open Control Stack is for. It's four small, open-source projects — Declare, Control, Replay, Evidence — that sit beside your agents and cover authorize, enforce, and prove. Declare comes first, and its artifact is the &lt;strong&gt;Agent Action Manifest&lt;/strong&gt;. This post describes the Action Manifest tooling in the Open Control Stack, available on &lt;a href="https://github.com/cogno-us/cognous-open-control-stack" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Actions, Written Down
&lt;/h2&gt;

&lt;p&gt;If you remember in post one, our AI agent was pulling customer data, drafted a cringey email, and sent it to our top 1,000 customers. The Action Manifest is used to outline the agent's permissions as declarations, to prevent this from ever happening again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer-service-agent-manifest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agent_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer-service-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-platform-team"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer relationship management system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"allowed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"external_system"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crm.internal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data_classification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer_pii"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Outbound customer email system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"allowed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"external_system"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smtp.internal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data_classification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer_pii"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"actions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pull_top_customers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Retrieve top customer records from the CRM."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reliance_requirement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allowed_source_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft_reply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Draft a reply email for a customer inquiry."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"authority_required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email.draft.customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Permission to draft outbound customer emails"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-platform-team"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"review_requirement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft_first"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pull_contract_details"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Retrieve contract terms from the CRM."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"contract"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"restricted"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"external_send"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Send an approved email to the customer."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"escalate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"authority_required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email.send.customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Outbound customer send scope"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email.send.customer"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"review_requirement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approval_required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"reviewer_role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-lead"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reliance_requirement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allowed_source_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_input"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"payload_policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sensitive_fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"customer_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer_name"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"forbidden_fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"contract_terms"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"redaction_hints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"field_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PII in exported records"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Field names here follow the public schema — check the &lt;a href="https://github.com/cogno-us/cognous-open-control-stack" rel="noopener noreferrer"&gt;repo&lt;/a&gt; for the exact current shape if you're implementing against it.)&lt;/p&gt;

&lt;p&gt;Rules are only as good as what is defined. At the very top of the manifest, we explicitly cover anything not mentioned: &lt;code&gt;"default_action": "block"&lt;/code&gt;. So when a new tool is onboarded (for example &lt;code&gt;send_tweet&lt;/code&gt;), you don't need to go back and update every manifest — it's blocked until you expressly give the agent permission to use it.&lt;/p&gt;

&lt;p&gt;For those actions that are defined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pull_top_customers&lt;/code&gt; and &lt;code&gt;draft_reply&lt;/code&gt; are both &lt;code&gt;allow&lt;/code&gt;, but not the same kind of allow: pulling the top customers is allowed, full stop, but the draft reply has &lt;code&gt;review_requirement.mode: draft_first&lt;/code&gt; — the agent can write it, but it stops there.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;send_email&lt;/code&gt; isn't just "escalate" as a vague gesture — it names the exact authority scope required (&lt;code&gt;email.send.customer&lt;/code&gt;), the exact reviewer role (&lt;code&gt;support-lead&lt;/code&gt;), and which payload fields have to be redacted before this record goes anywhere near an audit export.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pull_contract_details&lt;/code&gt; is the odd one out — it doesn't strictly need to be there. &lt;code&gt;default_action: block&lt;/code&gt; already covers anything undeclared, so an unlisted "read contract details" action would be blocked anyway. It's included for visibility: a blocked action that shows up in the manifest, tagged &lt;code&gt;restricted&lt;/code&gt;, is a documented decision someone can point to. A blocked action that's simply never mentioned looks identical to an oversight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's important to note that the manifest is not enforcement. It doesn't stop the agent from doing anything — a runtime layer has to actually read it and act on it (that'll be described in the next post). What the manifest gives you is a single artifact that says, in one place, what &lt;em&gt;should&lt;/em&gt; happen — before the agent has run even once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Nine Action Types
&lt;/h2&gt;

&lt;p&gt;In our simple manifest above, we show three possible action types. The manifest supports nine. These classifications are what give a governance review the ability to distinguish a harmless read from something that should never happen without a human:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;read&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;retrieves or inspects information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;creates or modifies information, not necessarily sent externally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;external_send&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sends information outside the system boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;removes information or records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;export&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;packages or transfers information downstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;purchase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;initiates or prepares a purchase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;approves or authorizes a workflow step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;escalate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;routes to a human or supervisory process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;other&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;anything not otherwise classified&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A tool list alone can't tell you this. Knowing the agent "has access to email" tells you nothing about whether it can draft, send, or both, and what's supposed to happen before each.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Review Modes
&lt;/h2&gt;

&lt;p&gt;Action type says &lt;em&gt;what kind&lt;/em&gt; of thing an action is. Review mode says &lt;em&gt;who has to look at it, and when&lt;/em&gt; — and it's doing just as much governance work in our example as action type is. There are four:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no specific review posture declared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;human_review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a human is expected to look at it before execution or completion — no specific person named&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approval_required&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;approval is required from a &lt;strong&gt;named reviewer role&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft_first&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the agent produces a draft; it does not finish the action itself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference between &lt;code&gt;human_review&lt;/code&gt; and &lt;code&gt;approval_required&lt;/code&gt; is easy to miss but it matters: &lt;code&gt;human_review&lt;/code&gt; says "someone should look at this" without saying who. &lt;code&gt;approval_required&lt;/code&gt; is stricter — it names a reviewer role, and the validator enforces that: an &lt;code&gt;approval_required&lt;/code&gt; action with no &lt;code&gt;reviewer_role&lt;/code&gt; fails validation outright.&lt;/p&gt;

&lt;p&gt;That's why &lt;code&gt;send_email&lt;/code&gt; in our manifest uses &lt;code&gt;approval_required&lt;/code&gt; with &lt;code&gt;reviewer_role: "support-lead"&lt;/code&gt;, not &lt;code&gt;human_review&lt;/code&gt;. Sending customer email isn't "someone should probably glance at this" — it's "this specific role signs off, or it doesn't go out."&lt;/p&gt;

&lt;p&gt;Worth being precise about what &lt;code&gt;"support-lead"&lt;/code&gt; actually is here: a role string, nothing more. The manifest doesn't know who holds that role, doesn't know they're on Slack, and doesn't page anyone. Your enterprise-side plumbing converts "support lead" to Janice, and builds an alerting system in Slack to let her know that her approval is required. The public stack's job ends at "this role must approve, and there's a record of whether it did."&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliance: A Third Thing Being Tracked
&lt;/h2&gt;

&lt;p&gt;Authority asks whether an action was &lt;em&gt;allowed&lt;/em&gt;. Review asks whether a &lt;em&gt;person&lt;/em&gt; signed off. Reliance asks something different: what did this action actually &lt;em&gt;depend on&lt;/em&gt; to produce its result?&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;reliance_requirement&lt;/code&gt; declares that an action should leave a record of its source — a tool, a database, a file, an API, or user input. &lt;code&gt;pull_top_customers&lt;/code&gt; declares reliance on &lt;code&gt;tool&lt;/code&gt; and &lt;code&gt;database&lt;/code&gt;, because a governance review should be able to see it actually came from the CRM and not somewhere the agent invented. &lt;code&gt;send_email&lt;/code&gt; declares reliance on &lt;code&gt;tool&lt;/code&gt; and &lt;code&gt;user_input&lt;/code&gt;, tying the send back to the approved draft it was built from.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;draft_reply&lt;/code&gt; and &lt;code&gt;pull_contract_details&lt;/code&gt; don't declare a reliance requirement — which is why the summarize output above shows &lt;code&gt;Actions requiring reliance: 2&lt;/code&gt;, not 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation Isn't Optional, and It Isn't Decoration
&lt;/h2&gt;

&lt;p&gt;A manifest can be valid JSON, but be semantic nonsense. A &lt;code&gt;delete&lt;/code&gt; action on the production database marked &lt;code&gt;allow&lt;/code&gt; isn't caught by JSON Schema. It's caught by the validator, which checks things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;does every action reference a tool that's actually declared and allowed?&lt;/li&gt;
&lt;li&gt;is a privileged action type (&lt;code&gt;external_send&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;purchase&lt;/code&gt;, &lt;code&gt;approve&lt;/code&gt;) missing an authority requirement it should have?&lt;/li&gt;
&lt;li&gt;is an action marked &lt;code&gt;approval_required&lt;/code&gt; but missing a reviewer role?&lt;/li&gt;
&lt;li&gt;are there duplicate action or tool names?&lt;/li&gt;
&lt;li&gt;are sensitive fields declared with no redaction hints to match?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it from the CLI — &lt;code&gt;aam&lt;/code&gt;, short for Agent Action Manifest, is the command-line tool that ships with the manifest repo. First we run the validate command to ensure that the JSON is valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aam validate customer-service-agent-manifest.json
Validation result: VALID
Manifest ID:       customer-service-agent-manifest

Warnings &lt;span class="o"&gt;(&lt;/span&gt;3&lt;span class="o"&gt;)&lt;/span&gt;:
  &lt;span class="o"&gt;[&lt;/span&gt;W004] &lt;span class="o"&gt;[&lt;/span&gt;actions[1]&lt;span class="o"&gt;(&lt;/span&gt;draft_reply&lt;span class="o"&gt;)]&lt;/span&gt; Action &lt;span class="s1"&gt;'draft_reply'&lt;/span&gt; has effective default_action &lt;span class="s1"&gt;'allow'&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;action_type &lt;span class="s1"&gt;'write'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;W006] &lt;span class="o"&gt;[&lt;/span&gt;actions[1]&lt;span class="o"&gt;(&lt;/span&gt;draft_reply&lt;span class="o"&gt;)&lt;/span&gt;.payload_policy] Action &lt;span class="s1"&gt;'draft_reply'&lt;/span&gt; has action_type &lt;span class="s1"&gt;'write'&lt;/span&gt; but no payload_policy is declared.
  &lt;span class="o"&gt;[&lt;/span&gt;W005] &lt;span class="o"&gt;[&lt;/span&gt;actions[2]&lt;span class="o"&gt;(&lt;/span&gt;pull_contract_details&lt;span class="o"&gt;)&lt;/span&gt;.reliance_requirement] Action &lt;span class="s1"&gt;'pull_contract_details'&lt;/span&gt; has action_type &lt;span class="s1"&gt;'read'&lt;/span&gt; but no reliance_requirement is declared.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VALID, but not silent — the validator still flags things worth a second look, even in a manifest that passes. Fair warnings, too: we didn't bother declaring a payload policy for a draft-only action, and a blocked action doesn't need reliance evidence it'll never produce.&lt;/p&gt;

&lt;p&gt;Next we can summarize the manifest with the summarize command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aam summarize customer-service-agent-manifest.json
Manifest ID:          customer-service-agent-manifest
Agent name:           customer-service-agent
Environment:          production
Tools:                2
Actions:              4

Actions by &lt;span class="nb"&gt;type&lt;/span&gt;:
  external_send: 1
  &lt;span class="nb"&gt;read&lt;/span&gt;: 2
  write: 1

Actions by default posture:
  allow: 2
  block: 1
  escalate: 1

Actions requiring authority: 2
Actions requiring review:    2
Actions requiring reliance:  2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That "2" next to authority is easy to misread as just the send — it's actually &lt;code&gt;draft_reply&lt;/code&gt; and &lt;code&gt;send_email&lt;/code&gt; both. Drafting on a customer's behalf declares its own authority requirement (&lt;code&gt;email.draft.customer&lt;/code&gt;), separate from the one on the final send (&lt;code&gt;email.send.customer&lt;/code&gt;). Declaring it isn't the same as enforcing it — nothing here checks whether that authority actually exists. That check is the Control Plane's job, next post.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aam&lt;/code&gt; verifies that your manifest is ready, and gives the team a concise summary of what's allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Deliberately Doesn't Do
&lt;/h2&gt;

&lt;p&gt;The manifest is the &lt;strong&gt;Declare&lt;/strong&gt; layer, and it stays in its lane:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it doesn't execute the agent&lt;/li&gt;
&lt;li&gt;it doesn't enforce anything at runtime&lt;/li&gt;
&lt;li&gt;it doesn't grant authority — &lt;code&gt;email.send.customer&lt;/code&gt; being &lt;em&gt;declared&lt;/em&gt; as required doesn't mean it &lt;em&gt;exists&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;it doesn't replace your application's own authorization system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Declaring that sending email should require approval is not the same thing as stopping an unapproved send. That gap — between what's declared and what actually happens when the agent tries to act — is exactly what the next layer closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;The manifest says what the agent &lt;em&gt;may&lt;/em&gt; propose. It says nothing about what the agent actually &lt;em&gt;does&lt;/em&gt; on a Tuesday afternoon when it's mid-run and reaching for the send button. That's the &lt;strong&gt;Agent Control Plane&lt;/strong&gt; — it reads this exact manifest, sits beside the agent at runtime, and turns every proposed action into a recorded decision: allow, block, or escalate, deterministically, every time. That's next.&lt;/p&gt;

&lt;p&gt;The manifest repo, examples, schemas, and test suite are live now: &lt;a href="https://github.com/cogno-us/cognous-open-control-stack" rel="noopener noreferrer"&gt;github.com/cogno-us/cognous-open-control-stack&lt;/a&gt;. Clone it, validate the customer-service example, break it on purpose and watch the validator catch you.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>security</category>
    </item>
    <item>
      <title>Cognous Keeps Your AI in Check</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Thu, 06 Aug 2026 22:49:57 +0000</pubDate>
      <link>https://dev.to/cognous/cognous-keeps-your-ai-in-check-69l</link>
      <guid>https://dev.to/cognous/cognous-keeps-your-ai-in-check-69l</guid>
      <description>&lt;p&gt;You've probably gotten an email that was supposed to feel personal, but obviously wasn't. Something about it just felt off. Automated. And somewhere in the back of your mind, you wondered: who approved this?&lt;/p&gt;

&lt;p&gt;Honestly? The answer is probably no one.&lt;/p&gt;

&lt;p&gt;Organizations are leaning harder on AI agents to actually do work. Agents are making database queries, writing letters, accessing email lists, but where does the access end? Without solid guardrails, tone-deaf emails are no longer an embarrassing edge case, they're a real risk. Nobody wants to be the team that let an agent send 1,000 tone-deaf emails to its best clients before anyone noticed.&lt;/p&gt;

&lt;p&gt;This series is about the tooling that keeps that from happening, and about actually building it, not just reading about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually needs to happen
&lt;/h2&gt;

&lt;p&gt;The email itself might be perfectly fine. Correct grammar, on-brand, even genuinely well-written. But the problem is that the email didn't need to be sent. The agent just did it anyway, without any guidance from the team.&lt;/p&gt;

&lt;p&gt;So the fix isn't a better read of what the agent said. It's governance around what the agent is allowed to do, three capabilities, specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authorize&lt;/strong&gt;: Decide in advance what an agent is allowed to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce&lt;/strong&gt;: Apply that decision the moment the agent tries to act, before the action executes, not after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove&lt;/strong&gt;: Turn every run into a record you can produce on demand, not one you have to reconstruct from memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Application logs are a day late and a dollar short: the emails are already sent. Looking at logs is reactive. You need a stack that sits in front of your agents and proactively approves or denies actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Cognous
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cogno.us/" rel="noopener noreferrer"&gt;Cognous&lt;/a&gt; builds infrastructure for governing AI agents. Its first release, the Open Control Stack, is fully open source: four small projects that sit beside your AI agents to keep them in check, together covering Authorize, Enforce, and Prove. The four projects are &lt;strong&gt;Declare, Control, Replay, Evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Declare, the Agent Action Manifest&lt;/strong&gt;. Written once, before the agent ever runs. The manifest provides or denies access. The agent is attempting to complete a task, but the manifest keeps it in check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Pull top customers&lt;/em&gt;: &lt;code&gt;default_action: allow&lt;/code&gt;. A CRM read, flagged to leave a reliance record of what data it used.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Draft a reply&lt;/em&gt;: &lt;code&gt;default_action: allow&lt;/code&gt;, review_mode: draft_first. The agent prepares content; it doesn't finish the job.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Pull contract details&lt;/em&gt;: &lt;code&gt;default_action: block&lt;/code&gt;. Off-limits for this agent, full stop.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Send the email&lt;/em&gt;: &lt;code&gt;default_action: escalate&lt;/code&gt;. Requires a named authority scope and human sign-off before it's eligible to execute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is that the manifest lets you say &lt;em&gt;reading customer data is fine, reading contracts is not, drafting is fine, but sending the email needs a person&lt;/em&gt;: four different rules for four different actions, declared before any of them happen, instead of one blanket policy applied to "the agent" as if every action carried the same risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control, the Agent Control Plane&lt;/strong&gt;. Runs live, alongside the agent. Every time the agent tries to do something, the Control Plane checks it against what was declared, records the attempt, and either allows it, blocks it, or escalates it for approval, deterministically, every time. Every one of those outcomes gets written down, not just the bad ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replay, the Agent Replay Bundle&lt;/strong&gt;. Not a process, an export. When the run ends, everything the Control Plane recorded (what was proposed, what was decided, what was escalated, what it relied on) gets packaged into a single portable file. No more digging through the logs of a dozen systems to reconstruct one run. Every read, every draft, every escalation, everything that was allowed or held back: it's all in one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence, the Agent Governance Evidence Pack&lt;/strong&gt;. Assembled later, across many runs. This is where you get to show your work: not "trust us, we have controls," but "here's proof: 40 sends needed sign-off this month, and here's what happened to each one."&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking the scenario through the stack
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The manifest declares that sending an email requires authority and escalates rather than executing automatically, decided before the agent ever runs a single task.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mid-run, the Control Plane checks each of the agent's proposals against the manifest as they happen: pulling top customers and drafting a reply go through, pulling contract details is blocked outright, and the proposed send doesn't execute, it escalates and gets held for a person to review. Every one of those outcomes gets recorded as it happens, not just the one that got stopped.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When the run ends, every step (the reads, the draft, the block, the held send, and the outcome) gets packaged into a replay bundle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Over time, those bundles roll up into an evidence pack: not "we think our agent behaves," but "here's the record of every send this month that needed sign-off, and what happened to it."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A thousand cringey emails to customers drops to zero. There are no questions about why, no furious Slack thread trying to piece it together. And if anyone does wonder, the stack's replay bundle provides the paper trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it yourself
&lt;/h2&gt;

&lt;p&gt;The Open Control Stack, Cognous's first release, is fully open source, and everything above (the manifest fields, the allow/block/escalate decisions, all of it) comes straight from the actual code, not a slide deck. If you want to see AI agent governance in action instead of taking our word for it, the manifest, control plane, replay bundle, and evidence pack repos are all live on &lt;a href="https://github.com/cogno-us/cognous-open-control-stack" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Clone one, run the examples, watch a proposed action get authorized, blocked, or escalated in real time. You can learn more about Cognous at &lt;a href="https://cogno.us/" rel="noopener noreferrer"&gt;cogno.us&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is the first post in a series on AI agent governance. Next up, we build the manifest from this exact scenario: the real schema, the real validation rules, and what runtime authorization for AI agents actually looks like as working code. From there we'll get into the AI agent control plane that enforces it and how a single run turns into replayable AI agent evidence you can hand to an auditor. More posts coming soon.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>security</category>
    </item>
    <item>
      <title>When should you use canary deployments?</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Mon, 20 Apr 2026 18:42:22 +0000</pubDate>
      <link>https://dev.to/coderabbitai/when-should-you-use-canary-deployments-2ch9</link>
      <guid>https://dev.to/coderabbitai/when-should-you-use-canary-deployments-2ch9</guid>
      <description>&lt;p&gt;One solution for a tricky or high-risk deployment is to leverage a canary deployment. In a canary deployment, new features are rolled out to a small subset of your customer base with no fanfare. If things go well: hooray! Canary deployments are ideal for times when a deployment might go sideways or the feature might not work as expected. By utilizing a canary release, you have minimized the risk, as impacting a small fraction of your users is a lot better than affecting all of them.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx8m8c1v3v3l5p1xt6w2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx8m8c1v3v3l5p1xt6w2.png" alt="Deploying a Canary Release" width="512" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post will discuss when development teams might choose to use canary deployments in their applications, what is a canary deployment, common pitfalls to avoid, and how modern tooling can help ease the rollout of canary deployments.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is a canary deployment?
&lt;/h1&gt;

&lt;p&gt;Where does the term canary deployment come from? You may have heard of the term "canary in the coal mine." Early coal miners would keep caged songbirds in the mine with them, because these small birds are highly susceptible to toxic gases. If the canaries suddenly died, the miners knew that there was a dangerous gas release, and they could evacuate the mine before they succumbed to the deadly gas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgejryztof28vo7uoxlmq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgejryztof28vo7uoxlmq.jpg" alt="A canary in a coal mine" width="512" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A canary deployment can be performed when there is concern about the risks involved. Perhaps you are rolling out new cloud infrastructure that has not yet been tested with the production environment, or maybe there are performance concerns—the updates might cause the website's performance to drop to a terrible crawl. Perhaps the change is small, but it could not be tested for security vulnerabilities. A failed rollout to your entire customer base could end in disaster in all of these cases.&lt;/p&gt;

&lt;h1&gt;
  
  
  Is your team already using canary deployments?
&lt;/h1&gt;

&lt;p&gt;Many teams practice canary-style releases without explicitly calling them that. Common variations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blue-green deployments&lt;/strong&gt;: Two production environments run in parallel. The blue environment serves the stable version, while green runs the new version. Traffic is gradually shifted from blue to green, limiting risk and enabling fast rollback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experimental deployments&lt;/strong&gt;: New behavior is exposed to a defined group of users, often with explicit awareness or opt-in. These are typically broader than canary deployments and focus on measuring user impact rather than system health alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradual rollouts (progressive delivery)&lt;/strong&gt;: After validating a release with a small subset of users, traffic is incrementally increased (for example, 5% → 25% → 100%) while monitoring key metrics like error rates and latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow deployments&lt;/strong&gt;: The new version runs in parallel and receives a copy of real production traffic, but responses are not served to users. This is useful for validation and performance testing without customer impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Planning a canary deployment
&lt;/h1&gt;

&lt;p&gt;Imagine this situation: You are about to launch a new feature that is considered high-risk for some reason. The team has opted for a canary deployment, releasing it to a limited group of users first. Achieving a successful deployment in this scenario demands meticulous planning.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deployment audience&lt;/strong&gt;: Who gets the update? This requires a basic knowledge of your audience and understanding of who would benefit the most from the change. If your new version is an overhaul of the mobile dashboard, and the canary release is delivered to desktop users, how can you determine if the deployment was successful? It is certainly useful to have some desktop users in the cohort (in case something breaks on desktop), but in order to best understand the results of the deployment, you want to ensure that users interact with the features of the new version.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Duration&lt;/strong&gt;: How long will the test run? An hour? A week? Depending on the type of deployment, you may know the success/failure quickly. (Did the database correctly sync with the servers?). However, sometimes it may take days to determine success. (Is website performance faster during busy periods?).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Metrics&lt;/strong&gt;: What will the team track during the deployment? What metrics will be considered a success? What metrics would be branded a partial success? When does the team admit failure and roll back the deployment to fix bugs?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Feature flags&lt;/strong&gt;: For some canary deployments, using feature flags can be helpful. Feature flags are tools in your code that manage the deployment of application features to specific audiences. If your team is planning to use canary deployment extensively, it may be useful to integrate feature flags into your codebase.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Deployment risks
&lt;/h1&gt;

&lt;p&gt;With any release, there is a risk of failure. Using a canary deployment mitigates the risk by reducing the blast radius of a failure to a small subset of users—however, even canary releases have risks. Among these are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service outages&lt;/strong&gt;: If the deployment goes bad, the application may slow down or even cease to work for the selected users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security breaches&lt;/strong&gt;: The new release may introduce a security vulnerability that was not discovered in testing.
Rollback mechanism: Many canary releases do not have a formalized rollback plan, should things go wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data loss&lt;/strong&gt;: If the canary rollout is using a new database or a new interface to the database, any issue with the rollout may introduce data inconsistencies or data loss.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For any release, it is important to consider the risks and how they will be mitigated.  While the canary release lowers the risks, having plans for rollback, handling data loss etc. should be made prior to the release.&lt;/p&gt;

&lt;h1&gt;
  
  
  Key metrics during canary deployments
&lt;/h1&gt;

&lt;p&gt;During any release, it is critical to monitor your systems in case of any trouble during the deployment. Canary releases are no different, although the experimental nature of the release means there is a higher likelihood of issues arising:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error rates&lt;/strong&gt;: Are your systems throwing more errors than typically seen in production?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System metrics&lt;/strong&gt;: Keep a close eye on memory, CPU, database, and API queries, and other resource metrics. We've all been a part of a release where the new code introduced a memory leak, or suddenly API usage was pegging 400% of normal. By monitoring these in the canary release, the issues can be quickly remediated for the canary users before releasing to the entire base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic distribution&lt;/strong&gt;: Is the canary release being properly served to the selected users? Are users accidentally crossing between the two environments? Handling these issues early ensures that the new build is completely isolated from the old environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security metrics&lt;/strong&gt;: Is the login behavior different in the canary build? Are users interacting with this build differently than the previous version? Could this imply a potential threat?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Monitoring these metrics during the deployment and after the release can help your team better understand the wins (and potential improvements to your app) that occur during the canary release. In addition to traditional monitoring, there are now AI tools that can be used to compare your two environments, quickly parsing the data and determining differences from the old version and the new software version that might be difficult to see in traditional logging.&lt;/p&gt;

&lt;h1&gt;
  
  
  Canary deployments: What's next?
&lt;/h1&gt;

&lt;p&gt;When dealing with a high-risk release, a canary deployment to a small subset of users is a great way to mitigate exposure in production. To be successful in your canary release, be sure to discuss how you will choose the cohort of users, monitor the release, and plan a rollback strategy prior to the release. Then, during the deployment process, your team can track the essential metrics to understand if the canary deployment was a success. Should the deployment go well, the team can plan to add users to the new version of the software, slowly deprecating the old version.&lt;/p&gt;

&lt;p&gt;Developers who use canary releases for risky deployments are able to test problematic code or infrastructure to a small user base, pushing the product ahead while mitigating the risk of an outage.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>sre</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Database branching in Django apps using GitHub actions</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Mon, 02 Dec 2024 18:36:27 +0000</pubDate>
      <link>https://dev.to/hackmamba/database-branching-in-django-apps-using-github-actions-3lgh</link>
      <guid>https://dev.to/hackmamba/database-branching-in-django-apps-using-github-actions-3lgh</guid>
      <description>&lt;p&gt;Creating online previews of your applications is a great way to test that all the required functionality is present. When building and testing a dev build of your application from a pull request (PR), the last thing you want is for your tests to affect your production database.  Using a test branch of the production database ensures that the production database remains untouched, ensuring no accidental deletion of data or adding test data into the production database.&lt;/p&gt;

&lt;p&gt;In this post, we’ll create a set of GitHub Actions to automate the testing process of a pull request. Our GitHub Action will run when the pull request is created, generate a test branch of the production database, and deploy the code to Digital Ocean.  Once the PR is merged, a second GitHub Action will rebuild the Digital Ocean app with production code (and database), and the test database branch will be deleted.&lt;/p&gt;

&lt;p&gt;By finishing this article, you’ll be able to automate the creation of app previews using NeonDB branches, Digital Ocean, and Django. Let’s jump right in!&lt;/p&gt;

&lt;h1&gt;
  
  
  Setup
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Code repo
&lt;/h2&gt;

&lt;p&gt;To begin, we’ll use the &lt;a href="https://github.com/evanshortiss/django-neon-quickstart" rel="noopener noreferrer"&gt;Django Neon&lt;/a&gt; &lt;a href="https://github.com/evanshortiss/django-neon-quickstart" rel="noopener noreferrer"&gt;q&lt;/a&gt;&lt;a href="https://github.com/evanshortiss/django-neon-quickstart" rel="noopener noreferrer"&gt;uickstart&lt;/a&gt; repo on GitHub. Create a fork and clone your fork locally.  Follow the setup instructions in the README, and run the application locally to ensure it is up and running. (You’ll need &lt;a href="https://console.neon.tech/" rel="noopener noreferrer"&gt;a NeonDB account&lt;/a&gt; to add the environmental variables that connect to NeonDB and power the app.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Digital Ocean
&lt;/h2&gt;

&lt;p&gt;Digital Ocean has a simplified process for launching applications on its platform. You can be up and running in minutes with just a few configuration steps.&lt;/p&gt;

&lt;p&gt;You’ll need an account at &lt;a href="https://cloud.digitalocean.com/" rel="noopener noreferrer"&gt;Digital Ocean&lt;/a&gt; to deploy your application. From the Digital Ocean dashboard, select “Create” → App Platform.  Connect to GitHub, and choose the django-neon-quickstart, branch main. Click next. I ran this on the $5/month instance. In the “build phase,” click edit and add the three commands below:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install -r requirements.txt
python manage.py makemigrations
python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;These are the steps you ran to get the repo running locally—we’re just repeating it on Digital Ocean. &lt;/p&gt;

&lt;p&gt;Update the “Run” command to use gunicorn:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gunicorn django_neon.wsgi:application --bind 0.0.0.0:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Finally, update the HTTP Port to 8000 to match the port in the repository.  &lt;/p&gt;

&lt;p&gt;In step two of the setup process, update the environmental variables. You can do a bulk upload and copy and paste in your &lt;code&gt;.env&lt;/code&gt; from the local repo as shown below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbjeaks4y1j7kag461eot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbjeaks4y1j7kag461eot.png" width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click through the rest of the commands, and on completion, the app will deploy and be available for use on the internet.&lt;/p&gt;

&lt;p&gt;Note that in this repository the deployed code is the &lt;code&gt;main&lt;/code&gt; branch. We would like to display the PR preview from the &lt;em&gt;dev&lt;/em&gt; branch. (Change the names &lt;em&gt;main&lt;/em&gt; and &lt;em&gt;dev&lt;/em&gt; to whatever branches you wish to preview.)  Let’s continue our setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Secrets
&lt;/h2&gt;

&lt;p&gt;We need to add three GitHub secrets to the repository. Add secrets by clicking “Settings” on the top menu. Then: “Secrets and Variables” → Actions, and add repository secrets.&lt;/p&gt;

&lt;p&gt;Here are the three secrets to be added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;DO_KEY&lt;/em&gt;&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; A key from Digital Ocean with scopes to create, read, update, and delete apps.  You can create this from the Digital Ocean dashboard under API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;NEON_API_KEY&lt;/em&gt;&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Create your Neon API key at the NEON dashboard. Click your Avatar in the upper right corner, select Account Settings, and then choose API keys to create an API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;NEON_PW&lt;/em&gt;&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; This is the &lt;code&gt;PGPASSWORD&lt;/code&gt; from the .env file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Digital Ocean app spec files
&lt;/h2&gt;

&lt;p&gt;The App Spec file defines how the build process will be run at Digital Ocean. You can find your App Spec file in your App’s dashboard under “Settings.” (It will be autogenerated when you create your project.)&lt;/p&gt;

&lt;p&gt;In your GitHub Repository, create a &lt;code&gt;.do&lt;/code&gt; directory, and make two copies of the App Spec file: &lt;em&gt;app.yaml&lt;/em&gt; and &lt;em&gt;default.yaml&lt;/em&gt;. These will be used by our GitHub Actions to edit the Digital Ocean Application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;app.yaml is what will be provisioned on a pull request being opened:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;alerts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DEPLOYMENT_FAILED&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DOMAIN_FAILED&lt;/span&gt;
    &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;buildpack-stack=ubuntu-22&lt;/span&gt;
    &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;django-neon-quickstart&lt;/span&gt;
        &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*seal-app-dev&lt;/span&gt;&lt;span class="err"&gt;**&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nyc&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;build_command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
        &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
        &lt;span class="s"&gt;python manage.py makemigrations&lt;/span&gt;
        &lt;span class="s"&gt;python manage.py migrate&lt;/span&gt;
      &lt;span class="na"&gt;environment_slug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python&lt;/span&gt;
      &lt;span class="na"&gt;envs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PGHOST&lt;/span&gt;
        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RUN_AND_BUILD_TIME&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*new_host&lt;/span&gt;&lt;span class="err"&gt;**&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PGDATABASE&lt;/span&gt;
        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RUN_AND_BUILD_TIME&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neondb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PGUSER&lt;/span&gt;
        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RUN_AND_BUILD_TIME&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neondb_owner&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PGPASSWORD&lt;/span&gt;
        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RUN_AND_BUILD_TIME&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*new_password&lt;/span&gt;&lt;span class="err"&gt;**&lt;/span&gt;
      &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*dev&lt;/span&gt;&lt;span class="err"&gt;**&lt;/span&gt;
        &lt;span class="na"&gt;deploy_on_push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dougsillars/django-neon-quickstart&lt;/span&gt;
      &lt;span class="na"&gt;http_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
      &lt;span class="na"&gt;instance_count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;instance_size_slug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps-s-1vcpu-1gb-fixed&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;django-neon-quickstart&lt;/span&gt;
      &lt;span class="na"&gt;run_command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gunicorn django_neon.wsgi:application --bind 0.0.0.0:8000&lt;/span&gt;
      &lt;span class="na"&gt;source_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are four changes made in this file from the original at Digital Ocean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;name: add “-dev” to the end of the app name.&lt;/li&gt;
&lt;li&gt;PGHOST: Value should be &lt;code&gt;new_host&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PGPASSOWRD:value should be &lt;code&gt;new_password&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;github:branch &lt;code&gt;dev&lt;/code&gt; replaces main.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;When our GitHub Action runs:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The name of the application will change in the Digital Ocean dashboard, allowing the dev to see that the current state is a dev state.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;new_host&lt;/em&gt; and &lt;em&gt;new_password&lt;/em&gt; will be programmatically updated with the values from our newly created NeonDB branch.
&lt;/li&gt;
&lt;li&gt;We want to deploy the dev branch of our code to Digital Ocean to see the changes.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;default.yaml is used to revert the App Spec to production.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;default.yaml&lt;/em&gt; has two changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;name:  Add “-prod” to the end of the name.&lt;/li&gt;
&lt;li&gt;PGPASSWORD:value: &lt;code&gt;new_password&lt;/code&gt; replaces the password. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(We’re not showing the &lt;a href="https://github.com/dougsillars/django-neon-quickstart/blob/main/.do/default.yaml" rel="noopener noreferrer"&gt;entire file&lt;/a&gt; here for space reasons.)&lt;/p&gt;

&lt;p&gt;This will change the name in the Digital Ocean dashboard to show that prod is visible.  The password will revert to the original password from the primary branch of the database, and since the branch is main, the build will be the main branch.&lt;/p&gt;

&lt;p&gt;With these changes, we are now ready to begin implementing our two GitHub Actions: “Create NeonDB Branch” and “Destroy NeonDB Branch.”&lt;/p&gt;

&lt;h1&gt;
  
  
  Create NeonDB branch
&lt;/h1&gt;

&lt;p&gt;When a pull request is made to the main branch, this GitHub Action will fire and do a number of steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a development branch of the NeonDB database.

&lt;ul&gt;
&lt;li&gt;Grab the host and password of this new DB.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Check out the GitHub Code.&lt;/li&gt;
&lt;li&gt;Use a sed command to replace new_host &amp;amp; new_password placeholders in the .do/app.yaml file with the variables extracted in step 1a.
&lt;/li&gt;
&lt;li&gt;Install the Digital Ocean CLI.&lt;/li&gt;
&lt;li&gt;Update the App Spec with the new yaml file.&lt;/li&gt;
&lt;li&gt;Initiate a Digital Ocean deployment.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Neon Branch and deploy dev to DO&lt;/span&gt;
    &lt;span class="na"&gt;run-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create a Neon Branch 🚀&lt;/span&gt;
    &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Create-Neon-Branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify NEON API Key presence&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;if [ -z "${{ secrets.NEON_API_KEY }}" ]; then&lt;/span&gt;
                &lt;span class="s"&gt;echo "NEON_API_KEY is empty"&lt;/span&gt;
              &lt;span class="s"&gt;else&lt;/span&gt;
                &lt;span class="s"&gt;echo "NEON_API_KEY is set"&lt;/span&gt;
              &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Neon Branch&lt;/span&gt;
            &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create-branch&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neondatabase/create-branch-action@v5&lt;/span&gt;
            &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;project_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orange-violet-68318343"&lt;/span&gt;
              &lt;span class="c1"&gt;# optional (defaults to your primary  branch)&lt;/span&gt;
              &lt;span class="na"&gt;parent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt; 
              &lt;span class="c1"&gt;# optional (defaults to neondb)&lt;/span&gt;
              &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;neondb"&lt;/span&gt;
              &lt;span class="na"&gt;branch_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;development"&lt;/span&gt;
              &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;neondb_owner"&lt;/span&gt;
              &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.NEON_API_KEY&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo db_url ${{ steps.create-branch.outputs.db_url }}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo host ${{ steps.create-branch.outputs.host }}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo branch_id ${{ steps.create-branch.outputs.branch_id }}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Replace variables in YAML&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;sed -i 's|new_host|'"${{ steps.create-branch.outputs.host }}"'|g' .do/app.yaml&lt;/span&gt;
              &lt;span class="s"&gt;sed -i 's|new_password|'"${{ steps.create-branch.outputs.password }}"'|g' .do/app.yaml&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install doctl&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;digitalocean/action-doctl@v2&lt;/span&gt;
            &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DO_KEY }}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set environment variables&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;doctl auth init -t ${{ secrets.DO_KEY }}&lt;/span&gt;
              &lt;span class="s"&gt;# Update the app with the new specifications from neon&lt;/span&gt;
              &lt;span class="s"&gt;#  use active project id from DO url&lt;/span&gt;
              &lt;span class="s"&gt;doctl apps update 3aec3cab-fca5-4829-b5f4-1fd9d41b16a9  --spec .do/app.yaml&lt;/span&gt;
              &lt;span class="s"&gt;doctl apps create-deployment 3aec3cab-fca5-4829-b5f4-1fd9d41b16a9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hints on creating your action:&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The NeonDB  project_id is in the URL string when you load the project in the NeonDB dashboard.&lt;/li&gt;
&lt;li&gt;The UUID for your Digital Ocean application is also found in the dashboard URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save this workflow in &lt;code&gt;/.github/workflows&lt;/code&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Delete NeonDB branch
&lt;/h1&gt;

&lt;p&gt;Once the PR has been tested and approved, we want to destroy the NeonDB branch and revert the Digital Ocean deployment back to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The steps in this are:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Delete the NeonDB development branch.&lt;/li&gt;
&lt;li&gt;Check out the code.&lt;/li&gt;
&lt;li&gt;Update the &lt;em&gt;default.yaml&lt;/em&gt; with our DB password from the GitHub Secrets.&lt;/li&gt;
&lt;li&gt;Update the App Spec and deploy the application at Digital Ocean.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete Neon Branch with GitHub Actions Demo&lt;/span&gt;
    &lt;span class="na"&gt;run-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete a Neon Branch 🚀&lt;/span&gt;
    &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;closed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;delete-neon-branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete Neon branch&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neondatabase/delete-branch-action@v3&lt;/span&gt;
            &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;project_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orange-violet-68318343"&lt;/span&gt;
              &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
              &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NEON_API_KEY }}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Replace variables in YAML&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;sed -i 's|new_password|'"${{ secrets.NEON_PW }}"'|g' .do/default.yaml&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install doctl&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;digitalocean/action-doctl@v2&lt;/span&gt;
            &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DO_KEY }}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set environment variables&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;doctl auth init -t ${{ secrets.DO_KEY }}&lt;/span&gt;
              &lt;span class="s"&gt;# Update the app with the new specifications from neon&lt;/span&gt;
              &lt;span class="s"&gt;#  use active project id from DO url&lt;/span&gt;
              &lt;span class="s"&gt;doctl apps update 3aec3cab-fca5-4829-b5f4-1fd9d41b16a9  --spec .do/default.yaml&lt;/span&gt;
              &lt;span class="s"&gt;doctl apps create-deployment 3aec3cab-fca5-4829-b5f4-1fd9d41b16a9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay, that is a &lt;em&gt;lot of code.&lt;/em&gt; Don’t forget to update the DO UUIDs to match your deployment. Push this all to your repo so that we can see our automation in action.&lt;/p&gt;

&lt;p&gt;Here is the production version of the application running on Digital Ocean. I added a few extra elements for fun. The screenshot shows the mouse hover color on &lt;strong&gt;Ne&lt;/strong&gt; &lt;strong&gt;(Neon)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzr4i1xd9sc6k9yapjto.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzr4i1xd9sc6k9yapjto.png" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let’s make some changes to the code and start a pull request.&lt;/p&gt;

&lt;p&gt;Create a dev branch for your code. Let’s change the colors in &lt;strong&gt;line 15&lt;/strong&gt; of/elements/templates/elements_list.html:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;li hx-delete="element/{{ element.id }}" hx-target="body" class="relative flex flex-col text-center p-5 rounded-md bg-[#7846a8] transition-colors hover:bg-orange-500 text-[white]"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This should make the boxes purple, with orange hover and white text.&lt;/p&gt;

&lt;p&gt;Push the dev branch and open a pull request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3b792balxxqvrhttt3gh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3b792balxxqvrhttt3gh.png" width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the pull request is created, the “Create a Neon Branch” GitHub Action is called. A branch of the NeonDB is created, and the dev code is deployed to Digital Ocean.&lt;/p&gt;

&lt;p&gt;Refreshing the application, we see the colors have been updated:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqnqicsefwa0mpt6y4vw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqnqicsefwa0mpt6y4vw.png" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More importantly, we can test all we want without worrying about the production database. Any changes made on the dev branch are in the development branch of NeonDB. As a part of our testing, we deleted a number of entries—only Neon is left:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fauzscgwg47mem9sgj29p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fauzscgwg47mem9sgj29p.png" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since we’re happy with the PR, we can approve and merge the changes. This fires up the second GitHub Action: deleting the NeonDB development branch and pushing the production build to Digital Ocean:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjb5iugxkpq6vwege1t9m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjb5iugxkpq6vwege1t9m.png" width="800" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The new colors are in prod, and the prod database was untouched by our testing on the PR!&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this post, we used GitHub Actions to automate creating and deleting a NeonDB database for testing pull request builds on Digital Ocean.  If you would like to look at the code, it is available on &lt;a href="https://github.com/dougsillars/django-neon-quickstart" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. You’ll just need to wire in your NeonDB and Digital Ocean credentials to get up and running.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Automating Developer Relations Metrics with Low Code RunBooks</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Mon, 24 Apr 2023 22:05:02 +0000</pubDate>
      <link>https://dev.to/unskript/automating-developer-relations-metrics-with-low-code-runbooks-5ch2</link>
      <guid>https://dev.to/unskript/automating-developer-relations-metrics-with-low-code-runbooks-5ch2</guid>
      <description>&lt;p&gt;My job at unSkript is to spread awareness and excitement around the DevOps tooling we have built.  But, I am also expected to provide reporting on various metrics around developer awareness and usage of our product. In this post, I walk through how I have automated the data collection process, so that I can spend more time creating content and building awareness.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;A bit of background: at unSkript, we are building automation tools to reduce toil. In the DevOps/SRE space, toil is defined as the manual and repetitive work that needs to be done to keep everything shipshape.  If you ask me – collecting metrics from a bunch of different services (Github, Google Analytics, internal databases, Docker,….), and aggregating them in one place – that sounds like toil.  So let’s automate that away, and then I no longer have to think about it (until I decide to write a blog post about it, of course.)&lt;/span&gt;&lt;/p&gt;



&lt;h2&gt;&lt;span&gt;Collecting the Data&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;unSkript is a tool to help you build RunBooks. A RunBook is a collection of steps (we call them Actions) that complete a task.  For DevOps teams, that could be &lt;a href="https://unskript.com/security-checkup-force-aws-load-balancers-to-redirect-to-https/"&gt;auto-remediation of your load balancers&lt;/a&gt;, &lt;a href="https://unskript.com/runbook-analysis-of-k8s-logs/"&gt;running health checks on a K8s cluster&lt;/a&gt;, or even monitoring your &lt;a href="https://unskript.com/keeping-your-cloud-costs-in-check-automated-aws-cost-charts-and-alerting/"&gt;daily Cloud costs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I want to use these actions to collect a bunch of different data points, and store them all in one place. There are a few different ways that I use unSkript to collect the information:&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Built in Actions&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;unSkript comes with hundreds of built-in Actions – simply drag &amp;amp; drop into your RunBook, configure your credentials, and you are ready to go!  When using built-in Actions – unSkript can be thought of as essentially “no-code” to set up. There are several built-in Actions in unSkript that are well suited to collecting the data that I want to collect: Daily Unique users from Google Analytics, and the Github star count.  &lt;/span&gt;&lt;/p&gt;



&lt;p&gt;&lt;span&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zlNs234e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.31.00.jpg%3Fresize%3D300%252C85%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zlNs234e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.31.00.jpg%3Fresize%3D300%252C85%26ssl%3D1" alt="GA Action" width="300" height="85"&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sCgbVWJy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-14.44.41.jpg%3Fresize%3D300%252C80%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sCgbVWJy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-14.44.41.jpg%3Fresize%3D300%252C80%26ssl%3D1" alt="GitHub Star Action" width="300" height="80"&gt;&lt;/a&gt;NOTE: Github stars as a DevRel metric can be controversial (IMO- it is useful as an indicator metric) but feel free to leave a comment below with your thoughts.)&lt;/span&gt;&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Database Queries &lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;Many of our stats are collected from Segment, and stored in a database (and that database is awesome for in depth detailed analysis).  But I want to keep all of my high level statistics and data in one table, so, I’ll use the PostgreSQL connector to extract the datapoints I’d like into my dataset:&lt;/span&gt;&lt;/p&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6CEHmwkA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.53.29.jpg%3Fresize%3D300%252C232%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6CEHmwkA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.53.29.jpg%3Fresize%3D300%252C232%26ssl%3D1" alt="3 SQL Actions to add data" width="300" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These Actions are “low-code” in that once you drag &amp;amp; drop the action and make the connections, you still need to create a SQL query to grab the results.&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;REST API&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;There are still a few more data points that I’d like to pull out of other tools.  We have a REST API connector that makes this easy: set up your credentials and the headers you need – and you can create a new Action that extracts your data via API.  These are also “low-code” but do require some understanding of how to make API calls – in order to set up the credentials properly.&lt;/span&gt;&lt;/p&gt;



&lt;p&gt;&lt;span&gt;For example: Docker Hub publishes the number of times our Docker Image has been downloaded. We can collect this number Each day using the REST API Action – and adding the endpoint and headers to the Action:&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GI0NbCRr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.23.55.jpg%3Fresize%3D300%252C297%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GI0NbCRr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.23.55.jpg%3Fresize%3D300%252C297%26ssl%3D1" alt="" width="300" height="297"&gt;&lt;/a&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--J18tsC54--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.25.27.jpg%3Fresize%3D300%252C99%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J18tsC54--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.25.27.jpg%3Fresize%3D300%252C99%26ssl%3D1" alt="" width="300" height="99"&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Storing &amp;amp; reporting our data&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;Once we have collected all of the data, we can create a message and post it on Slack for the team to see:&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--s7yO2Xyb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.35.34.jpg%3Fresize%3D300%252C267%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--s7yO2Xyb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.35.34.jpg%3Fresize%3D300%252C267%26ssl%3D1" alt="Slack Action" width="300" height="267"&gt;&lt;/a&gt;The message that is sent to the channel is a &lt;a href="https://realpython.com/python-f-strings/"&gt;Python f string&lt;/a&gt; with variables added.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ou-lwH8K--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot_2023-04-24_at_12_05_04.jpg%3Fresize%3D945%252C315%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ou-lwH8K--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot_2023-04-24_at_12_05_04.jpg%3Fresize%3D945%252C315%26ssl%3D1" alt="" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;This is a fun way to update the team on a daily basis…but we also want to chart this data over time.  To accomplish this, we have a table in PostgreSQL for our stats, and we just make an INSERT using the prebuilt Postgres Action (again this is low-code as you must write the SQL INSERT command:&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iaS5m6fA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.37.49.jpg%3Fresize%3D300%252C64%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iaS5m6fA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.37.49.jpg%3Fresize%3D300%252C64%26ssl%3D1" alt="" width="300" height="64"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;The data in Postgres feeds a Grafana dashboard – allowing the team access to the latest data from our metrics – and the best part of it all is that there is no daily toil required.  And many folks on the team just go to the dashboard to get the data – the DevRel team is no longer a bottleneck!&lt;/span&gt;&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Progressive enhancement&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;As time goes on, more questions about data will arise. &lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;As an example, since the number of Actions and RunBooks in GitHub keeps increasing, I was recently asked “how many Actions do we have in GItHub today?” &lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;The first few times you are asked a question like this, you can probably get away with waving your hands, and a ballpark figure… but after being asked a few times, I knew I needed a “real” answer. Reusing an existing GitHub Action, I was able to create a file in Github with the counts that I needed.  &lt;/span&gt;&lt;span&gt;By dragging a new Action into my RunBook, writing a few lines of Python code (and a small change on the Postgres insert), I was able to easily extend the current data collection to include more data.  &lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--x1LDTNH9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.43.42.jpg%3Fresize%3D300%252C134%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--x1LDTNH9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.43.42.jpg%3Fresize%3D300%252C134%26ssl%3D1" alt="" width="300" height="134"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Aside: We can also leverage these values to create custom badges for the Github readme, and on the website – so creating the data has been a double win!&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YL2HuT7c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.44.33.jpg%3Fresize%3D300%252C148%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YL2HuT7c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.44.33.jpg%3Fresize%3D300%252C148%26ssl%3D1" alt="" width="300" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Scheduling&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;Now that I have built a RunBook that collects all of data we need (so far…) -&amp;gt; I want to automate the execution of the RunBook.  Using unSkript’s Scheduler, I have set my RunBook to run at midnight GMT every day.  &lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mXByz9xz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-15.32.28.jpg%3Fresize%3D945%252C549%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mXByz9xz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-15.32.28.jpg%3Fresize%3D945%252C549%26ssl%3D1" alt="" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, I have daily reports being created for the team, that require ZERO work on my part!&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Summary&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;Collecting and aggregating statistics via automation frees the team to can focus on our “real” work: creating more tools and applications – and no longer spend significant time on metric collection.  At the same time, everyone has visibility into the project – showing the value of the DevRel team, without impacting their workload.&lt;/span&gt;&lt;/p&gt;


&lt;p&gt;&lt;span&gt;How does your DevRel team collect usage data?  If you’d like to give unSkript a try, check out our&lt;a href="https://us.app.unskript.io/"&gt; Free trial&lt;/a&gt;.  Join our &lt;a href="https://communityinviter.com/apps/cloud-ops-community/awesome-cloud-automation"&gt;Slack Channel&lt;/a&gt;, and I would be happy to chat with you on strategies to build your RunBook to collect your analytics data.  I’ll also be happy to share the skeleton of my RunBook to get you started!&lt;/span&gt;&lt;/p&gt;

</description>
      <category>blog</category>
      <category>intelligentautomatio</category>
      <category>leadership</category>
      <category>otherposts</category>
    </item>
    <item>
      <title>Automating the GitHub *Nudge*</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Tue, 04 Apr 2023 16:46:28 +0000</pubDate>
      <link>https://dev.to/unskript/automating-the-github-nudge-3i3k</link>
      <guid>https://dev.to/unskript/automating-the-github-nudge-3i3k</guid>
      <description>&lt;p&gt;Git is a tool of Actions.  Millions of times a day, users checkout, push, pull and merge submissions to their repositories.  The scale is staggering:over 3.5 billion &lt;a href="https://octoverse.github.com/2022/developer-community"&gt;contributions were made on GitHub in 2022&lt;/a&gt;.  That’s 227 million Pull Requests merged, and over 31 million issues closed.  However, What about the PRs and the issues that fall through the cracks and are forgotten?  Should they be just left, forgotten (like the Island of Misfit toys in Rudolf the RedNosed Reindeer?)&lt;/p&gt;

&lt;p&gt;In this post, we introduce an automated RunBook that introduces the “Github nudge.” Merriam-Webster’s &lt;a href="https://www.merriam-webster.com/dictionary/nudge"&gt;definition&lt;/a&gt; for a nudge is “to prod lightly &lt;strong&gt;: &lt;/strong&gt;urge into action.”  The GitHub nudge identifies issues and PRs that have been sitting a while, and “nudges” the assignee to take a look.  By not letting the team forget that the issues exist – they are more likely to be acted upon!&lt;/p&gt;

&lt;p&gt;We’ve defined a few Actions in unSkript’s RunBook architecture to help us along this path:&lt;/p&gt;

&lt;h2&gt;Stale Issues&lt;/h2&gt;

&lt;p&gt;When issues have been assigned to a team member, but no work is being done on them, the issue has probably “gotten lost.”  Everyone has a lot to work on, and sometimes these issues just lose priority – or get superseded by other tasks.  That does not mean that they should just be ignored – resolving the issue will improve the project.&lt;/p&gt;

&lt;p&gt;In unSkript, there is an Action to find “stale” issues: that is issues that are over a certain age.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LmS7w-fH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-02-at-21.27.45.jpg%3Fresize%3D840%252C166%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LmS7w-fH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-02-at-21.27.45.jpg%3Fresize%3D840%252C166%26ssl%3D1" alt="GitHub Stale issues" width="800" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This Action takes 3 input parameters – the Github owner and repository and the threshold (in days) upon which you define an issue as stale.  In the case of our repository &lt;a href="https://github.com/unskript/Awesome-CloudOps-Automation"&gt;https://github.com/unskript/Awesome-CloudOps-Automation&lt;/a&gt;, the owner is unskript, the repo is Awesome-CloudOps-Automation, and we set the stale threshold at 14 days.  There’s no real science around that day -it felt like a good number for our team.&lt;/p&gt;

&lt;p&gt;The response is an array of issues that surpass that threshold. With this data, we can do some quick examinations:&lt;/p&gt;

&lt;h3&gt;Issues without an assignee&lt;/h3&gt;

&lt;p&gt;Issues that have not been assigned to a resource are not going to be worked on – we all have a lot on our plate already!  In the case of Awesome-CloudOps-Automation – these are all “good first issues” for those interested in contributing.  I like to scan this list once a week for changes – and to ensure that issues that &lt;strong&gt;do&lt;/strong&gt; need work are properly assigned.&lt;/p&gt;

&lt;h3&gt;Issues with an assignee&lt;/h3&gt;

&lt;p&gt;If an issue has been around for 2 weeks, and is not yet resolved – its good to check on them to see the status – and that action is being made:&lt;/p&gt;

&lt;p&gt;For example – it appears that issue 346 is assigned to me, and I am overdue with an update (oops!):&lt;/p&gt;

&lt;pre&gt;{'assignee': NamedUser(login="dougsillars"),
   'issue_number': 346,
   'title': '[Action]: GitHub Comment on an issue'}

&lt;/pre&gt;

&lt;p&gt;In this case, the PR was already merged, but not connected to the issue, so it could be closed (whew!).&lt;/p&gt;



&lt;h2&gt;Stale Pull Requests&lt;/h2&gt;

&lt;p&gt;We can do the same thing for Pull Requests:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GYuGXQ1n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.14.30.jpg%3Fresize%3D578%252C168%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GYuGXQ1n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.14.30.jpg%3Fresize%3D578%252C168%26ssl%3D1" alt="Stale PR Action in unSkript" width="578" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A stale PR almost feels &lt;em&gt;worse&lt;/em&gt; than a stale Issue.  When the work is completed, and ready (perhaps nearly ready) to be integrated into the codebase – there is a immediate improvement to the software.  When. PR just languishes, the code does not improve, and sometimes further improvements are blocked   The Action above gives a list of all PRs that are over a threshold (again we use 14 days) for a given GitHub repository. For example:&lt;/p&gt;

&lt;pre&gt;{331: '3 cost optimization runbooks'}&lt;/pre&gt;



&lt;p&gt;This output does not provide anyone that I can ‘nudge’, but we have another Action that we can use:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ibM2r1pg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.21.47.jpg%3Fresize%3D638%252C156%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ibM2r1pg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.21.47.jpg%3Fresize%3D638%252C156%26ssl%3D1" alt="Github PR reviewer action" width="638" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To generate all of the reviewers to nudge, I first create a list of the PR numbers that I obtained from the first Action (the output was named “stalePull”:&lt;/p&gt;

&lt;p&gt;oldPRs = []&lt;br&gt;
for pr in stalePull[1]:&lt;br&gt;
oldPRs.append(list(pr.keys())[0])&lt;br&gt;
print(oldPRs)&lt;/p&gt;

&lt;p&gt;The Get Pull Request Reviewer Action takes 3 inputs – owner, repository and PR number.&lt;/p&gt;

&lt;p&gt;Using the iteration command in unSkript, we can apply the list of pull requests (oldPRs) to the &lt;em&gt;pull_request_number&lt;/em&gt; variable:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--m2cSY24c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.24.50.jpg%3Fresize%3D300%252C236%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--m2cSY24c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.24.50.jpg%3Fresize%3D300%252C236%26ssl%3D1" alt="iterating through all the pull requests" width="300" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will call the Action once per pull request – giving a full output of each PR, and who is left to review.&lt;/p&gt;

&lt;pre&gt;331: ['jayasimha-raghavan-unskript', 'shloka-bhalgat-unskript']&lt;/pre&gt;

&lt;p&gt;With a little Python, we can turn this around to list every user, and the PRs they should look at.&lt;/p&gt;

&lt;pre&gt;U03QU1K184X': [267, 153]&lt;/pre&gt;

&lt;p&gt;You may notice that the username is different.  This is in anticipation of the next step:&lt;/p&gt;

&lt;h2&gt;The actual “nudge”&lt;/h2&gt;

&lt;p&gt;Ok, so we know whose issues and PR reviews are overdue.  How do we alert them?  At unSkript, the internal team uses Slack.  I have created a table that compares each Github username with their Slack ID (that’s the weird variable above).&lt;/p&gt;

&lt;p&gt;With the slack ID, I can now send a message to the team channel:&lt;/p&gt;

&lt;pre&gt;If you are listed below, can you please review the Pull requests next to your name? They have been open for 14 days.

&amp;lt;@U01UG9DRR7D&amp;gt;, please review pull requests [267, 153].
&amp;lt;@U03QU1K184X&amp;gt;, please review pull requests [267, 153].

&lt;/pre&gt;

&lt;p&gt;Adding the @ in front of the username creates an “at” in Slack, so these two users have just been effectively nudged to look at their PRs. They have now been nudged in Slack to go into GitHub and take a look at the work that is being forgotten.&lt;/p&gt;

&lt;h2&gt;Automating the Nudge!&lt;/h2&gt;

&lt;p&gt;Using the Enterprise version (or the &lt;a href="https://us.app.unskript.io/"&gt;free trial)&lt;/a&gt; of unSkript, you can schedule each RunBook.  The RUnBook I created for Awesome-CloudOps-Automation runs every Wednesday morning – alerting the team that there are some issues and pull requests that have been left behind and should be resolved.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--v_gUwbP_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.40.41.jpg%3Fresize%3D945%252C142%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--v_gUwbP_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.40.41.jpg%3Fresize%3D945%252C142%26ssl%3D1" alt="Slack Message" width="800" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How do you keep your Github issues and Pull requests chugging along?  Do you have suggestions on how you might improve this RunBook?  We’d love to hear about it in the &lt;a href="https://communityinviter.com/apps/cloud-ops-community/awesome-cloud-automation"&gt;unSkript Slack channel&lt;/a&gt;. Interested in trying out Github nudges with your team?  All of the Actions described above are in our &lt;a href="https://github.com/unskript/Awesome-CloudOps-Automation"&gt;Open source&lt;/a&gt; (Docker instructions are in the readme), and in our &lt;a href="https://us.app.unskript.io/"&gt;free Trial&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>github</category>
      <category>intelligentautomation</category>
      <category>runbook</category>
    </item>
  </channel>
</rss>
