<?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: Jude</title>
    <description>The latest articles on DEV Community by Jude (@judezh).</description>
    <link>https://dev.to/judezh</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%2F4097535%2F4ae4de71-2102-47b7-adf5-dd91d529e410.png</url>
      <title>DEV Community: Jude</title>
      <link>https://dev.to/judezh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/judezh"/>
    <language>en</language>
    <item>
      <title>We open-sourced our agent platform's governance layer: hexagonal architecture and governed execution</title>
      <dc:creator>Jude</dc:creator>
      <pubDate>Thu, 27 Aug 2026 13:57:32 +0000</pubDate>
      <link>https://dev.to/judezh/we-open-sourced-our-agent-platforms-governance-layer-hexagonal-architecture-and-governed-execution-aam</link>
      <guid>https://dev.to/judezh/we-open-sourced-our-agent-platforms-governance-layer-hexagonal-architecture-and-governed-execution-aam</guid>
      <description>&lt;p&gt;We open-sourced &lt;a href="https://github.com/soit-ai/soit" rel="noopener noreferrer"&gt;SOIT&lt;/a&gt; a few weeks ago. The one&lt;br&gt;
line version: an agent runtime and governance platform for teams that need AI&lt;br&gt;
agents to touch real enterprise systems without losing control.&lt;/p&gt;

&lt;p&gt;This post is about the part that is actually hard — not building agents, but&lt;br&gt;
being willing to let them run in production.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem isn't building agents. It's trusting them.
&lt;/h2&gt;

&lt;p&gt;Every team I talk to has the same story. The demo took a week and it was great.&lt;br&gt;
Then security, compliance and ops started asking questions, and the project&lt;br&gt;
parked itself at PoC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is this agent allowed to do, and who decided that?&lt;/li&gt;
&lt;li&gt;Where do its credentials live, and what leaks if a prompt goes wrong?&lt;/li&gt;
&lt;li&gt;Which hosts can it reach when a tool call makes an outbound request?&lt;/li&gt;
&lt;li&gt;What exactly did it do last Tuesday, and can we prove it step by step?&lt;/li&gt;
&lt;li&gt;What did that run cost, and which team pays for it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Frameworks answer none of these — they orchestrate calls and leave controls to&lt;br&gt;
you. Hosted platforms answer some, but you inherit their model choices, their&lt;br&gt;
data boundary and their pricing. Cloud-vendor agent services answer more, in&lt;br&gt;
exchange for the deepest lock-in of all.&lt;/p&gt;

&lt;p&gt;Our answer is to make governance a kernel concern rather than a patch applied&lt;br&gt;
afterwards.&lt;/p&gt;
&lt;h2&gt;
  
  
  Governed execution
&lt;/h2&gt;

&lt;p&gt;The idea is simple to state: &lt;strong&gt;every agent run is a governed run.&lt;/strong&gt; Chat turn,&lt;br&gt;
agent loop, or workflow run — everything flows through one runtime ledger&lt;br&gt;
(&lt;code&gt;Run&lt;/code&gt; / &lt;code&gt;RunStep&lt;/code&gt; / &lt;code&gt;Trace&lt;/code&gt;), and the same controls apply on every path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permissions&lt;/strong&gt; — tenant and workspace scoping on every resource, RBAC with
resource-level grants. Every record carries &lt;code&gt;tenant_id&lt;/code&gt; and &lt;code&gt;workspace_id&lt;/code&gt;;
there are no escape hatches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approved capabilities&lt;/strong&gt; — agents bind to models, tools, knowledge bases and
workflows through per-version allowlists. A tool from a plugin, an MCP server,
or a built-in adapter looks identical to the agent and passes the same checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret boundaries&lt;/strong&gt; — credentials live in Vault with workspace-scoped
visibility and are injected at the gateway. Business code never opens a raw
HTTP client or LLM SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Egress policy&lt;/strong&gt; — outbound HTTP from tools is policy-controlled. An agent
cannot quietly call a host you never approved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ledger, cost, audit, replay&lt;/strong&gt; — per-step tokens, latency and cost; a full
audit log of privileged operations; a trace timeline you can replay step by
step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of duties&lt;/strong&gt; — the Dev role that builds and runs agents cannot
change egress policy, secrets, or installed plugins. That takes a workspace
Owner or Admin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a wrapper around someone else's runtime. SOIT is a hexagonal&lt;br&gt;
architecture: a stable kernel, versioned JSON Schema contracts on every&lt;br&gt;
primitive, and replaceable adapters at the edges — so the governance layer holds&lt;br&gt;
no matter which model or vector store you swap in.&lt;/p&gt;

&lt;p&gt;That last property is the whole reason for the shape. Governance implemented at&lt;br&gt;
the integration layer has to be rewritten for every new tool and every new&lt;br&gt;
provider. Governance implemented at the port layer is written once, and every&lt;br&gt;
adapter inherits it whether it wants to or not.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;p&gt;Four pillars, all of them in the open-source edition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt; — visual agent assembly with versioning and release management, a
DAG workflow editor, a knowledge pipeline (PDF/DOCX/Markdown/HTML into
Milvus-backed retrieval), and MCP support: any Model Context Protocol server
resolves into the tool registry without code changes, including OAuth
2.1-protected servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute&lt;/strong&gt; — an outbox-based event-driven runtime with checkpoints, retries
and fallback chains; multi-model routing across OpenAI, Anthropic, DeepSeek,
Qwen and any OpenAI-compatible endpoint — including the one on your own GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe&lt;/strong&gt; — a workspace console built on the runtime ledger: live run
volume, cost burn, failure rates, drill-down by agent, workflow and tool, plus
OpenTelemetry tracing and Prometheus metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Govern&lt;/strong&gt; — everything in the section above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It self-hosts with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/soit-ai/soit.git
&lt;span class="nb"&gt;cd &lt;/span&gt;soit
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
docker compose &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env &lt;span class="nt"&gt;-f&lt;/span&gt; docker/docker-compose.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A supply chain you can verify
&lt;/h2&gt;

&lt;p&gt;If your agents run in production, so does your agent platform — which makes it&lt;br&gt;
part of your attack surface.&lt;/p&gt;

&lt;p&gt;Every SOIT release is built by a tag-triggered pipeline that publishes&lt;br&gt;
digest-addressable images, SPDX SBOMs, Sigstore-backed build provenance and SBOM&lt;br&gt;
attestations, a deterministic source archive, and &lt;code&gt;SHA256SUMS&lt;/code&gt;. v1.0.0 shipped&lt;br&gt;
that way: three images on GHCR that pull anonymously, and artifacts you can&lt;br&gt;
check with &lt;code&gt;gh attestation verify&lt;/code&gt; before they enter your environment — we ran&lt;br&gt;
it ourselves and got exit 0.&lt;/p&gt;

&lt;p&gt;Secret scanning, dependency audit and container scanning run in CI as gates, not&lt;br&gt;
as dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SOIT is not
&lt;/h2&gt;

&lt;p&gt;A post like this should also say what you are not getting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is not a lightweight chatbot builder. If you want a prompt box and a share
link, plenty of tools do that with far less infrastructure.&lt;/li&gt;
&lt;li&gt;Content safety and PII detection are &lt;strong&gt;not implemented&lt;/strong&gt;. SOIT exposes a
content-safety port and an HTTP adapter so you can plug in a classifier you
operate, and inspection outcomes become part of run evidence — but with no
adapter configured, no inspection happens. I would rather say that than ship a
checkbox that does nothing.&lt;/li&gt;
&lt;li&gt;The project is young. v1.0.0 is released and the CI gates are in place, but
there is no large-scale production deployment vouching for it yet. We run it
ourselves. Early adopters welcome, with eyes open.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  License: Apache 2.0
&lt;/h2&gt;

&lt;p&gt;Commercial use, self-hosting, internal deployments and building products on top&lt;br&gt;
are all unrestricted. The core platform is and will remain open source; SSO,&lt;br&gt;
advanced audit reports and multi-region deployment live in SOIT Enterprise. The&lt;br&gt;
commercial boundary is drawn in the feature set, not in the license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Come break it
&lt;/h2&gt;

&lt;p&gt;The repo is at &lt;strong&gt;&lt;a href="https://github.com/soit-ai/soit" rel="noopener noreferrer"&gt;github.com/soit-ai/soit&lt;/a&gt;&lt;/strong&gt;.&lt;br&gt;
The quickstart takes about ten minutes on a machine with Docker. There are&lt;br&gt;
&lt;code&gt;good first issue&lt;/code&gt;s seeded and the roadmap is pinned.&lt;/p&gt;

&lt;p&gt;If the architecture trade-offs are the interesting part for you — the outbox&lt;br&gt;
runtime, the spec-first contracts, lease-based worker recovery — say so and I&lt;br&gt;
will write those up separately. I also wrote a companion piece on what happens&lt;br&gt;
when an MCP tool call has to obey RBAC, secrets and egress policy:&lt;br&gt;
&lt;a href="https://dev.to/judezh/five-questions-to-answer-before-you-put-mcp-in-production-j58"&gt;Five questions to answer before you put MCP in production&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If your agents graduated from notebooks and hit the trust wall, this was built&lt;br&gt;
for you. Tell me where it falls short.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I maintain SOIT.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>architecture</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Five questions to answer before you put MCP in production</title>
      <dc:creator>Jude</dc:creator>
      <pubDate>Thu, 27 Aug 2026 13:46:50 +0000</pubDate>
      <link>https://dev.to/judezh/five-questions-to-answer-before-you-put-mcp-in-production-j58</link>
      <guid>https://dev.to/judezh/five-questions-to-answer-before-you-put-mcp-in-production-j58</guid>
      <description>&lt;p&gt;MCP standardized the boring part of giving an agent tools. One streamable HTTP&lt;br&gt;
endpoint, one &lt;code&gt;list_tools&lt;/code&gt; call, and the tools show up in the model's callable&lt;br&gt;
list. The first time we wired one up internally it took an afternoon.&lt;/p&gt;

&lt;p&gt;Then we tried to put it in production, and someone from security asked five&lt;br&gt;
questions. I could not answer any of them.&lt;/p&gt;

&lt;p&gt;This post is those five questions, and how we ended up answering them in SOIT —&lt;br&gt;
an open-source agent runtime with governance in the middle of it. Everything&lt;br&gt;
below points at a file in the repo, because posts like this are unusually easy&lt;br&gt;
to write as a slide deck instead of as software.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Who is allowed to call this tool?
&lt;/h2&gt;

&lt;p&gt;MCP does not have an opinion here. Whatever &lt;code&gt;list_tools&lt;/code&gt; returns is what the&lt;br&gt;
model can call. Visibility is capability.&lt;/p&gt;

&lt;p&gt;In a multi-tenant, multi-workspace deployment that is not enough. SOIT installs&lt;br&gt;
an MCP server as a &lt;strong&gt;plugin artifact&lt;/strong&gt; rather than as a config entry. Tool&lt;br&gt;
references are namespaced — &lt;code&gt;mcp_tool:{server}:{tool}&lt;/code&gt;, parsed by&lt;br&gt;
&lt;code&gt;parse_mcp_tool_ref&lt;/code&gt; in &lt;code&gt;server/app/adapters/tools/mcp.py&lt;/code&gt; — and every&lt;br&gt;
resolution carries a &lt;code&gt;RequestContext&lt;/code&gt; holding &lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;workspace_id&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;user_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things follow from that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;From the agent's point of view, a tool from a plugin, a tool from an MCP
server, and a built-in adapter all look identical. Bindings are typed and
versioned.&lt;/li&gt;
&lt;li&gt;Permission checks, secret injection, egress limits, audit, cost attribution,
trace and replay apply to MCP tools &lt;strong&gt;automatically&lt;/strong&gt;. Nobody writes the
governance path twice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each agent version also carries a capability allowlist covering models,&lt;br&gt;
knowledge bases, workflows, tools, plugins and MCP servers. So "which MCP tools&lt;br&gt;
can v3 of this agent call" is something you can diff and roll back, rather than&lt;br&gt;
a runtime toggle somebody flipped.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Where do the credentials live?
&lt;/h2&gt;

&lt;p&gt;Most MCP integration examples look like this:&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;"auth"&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;"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;"bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk-xxxxxxxx"&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;A plaintext token in a config file. It ends up in git. It ends up in logs. It&lt;br&gt;
ends up in the config backup somebody exported to a laptop.&lt;/p&gt;

&lt;p&gt;SOIT rejects this outright. &lt;code&gt;_build_auth_headers&lt;/code&gt; checks for a &lt;code&gt;token&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;value&lt;/code&gt; field in the auth config and raises:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCP credentials must use secret_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;secret_id&lt;/code&gt; is accepted, resolved through &lt;code&gt;SecretsPort&lt;/code&gt; at call time. Same&lt;br&gt;
for API keys — and they are only supported in headers, never in a query string,&lt;br&gt;
because query strings leak through logs and referrers.&lt;/p&gt;

&lt;p&gt;The real value exists in memory for the duration of the call and nowhere else.&lt;br&gt;
What gets persisted — to the database, to audit records, to traces — is a&lt;br&gt;
&lt;strong&gt;redacted copy&lt;/strong&gt;: &lt;code&gt;ToolPolicyGateway._resolve_secrets&lt;/code&gt; builds it in the same&lt;br&gt;
pass that resolves the secret, keeping only &lt;code&gt;secret_id&lt;/code&gt; and the signing policy&lt;br&gt;
reference (&lt;code&gt;server/app/kernel/ports/tools/policy.py&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Three auth types are supported: &lt;code&gt;bearer&lt;/code&gt;, &lt;code&gt;api_key&lt;/code&gt;, &lt;code&gt;oauth2&lt;/code&gt;. OAuth follows&lt;br&gt;
2.1 with authorization-server discovery (RFC 9728, RFC 8414 / OpenID Connect)&lt;br&gt;
and resource-bound tokens (RFC 8707), using the &lt;code&gt;client_credentials&lt;/code&gt; grant.&lt;/p&gt;

&lt;p&gt;One limitation worth stating plainly: &lt;strong&gt;the browser-based authorization_code&lt;br&gt;
flow is not implemented.&lt;/strong&gt; SOIT calls MCP servers on its own behalf, not on&lt;br&gt;
behalf of a user sitting in front of a browser. If you need "call a protected&lt;br&gt;
MCP server as the end user," this does not cover you.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Where can it connect to?
&lt;/h2&gt;

&lt;p&gt;This is the one that should worry you most.&lt;/p&gt;

&lt;p&gt;You deployed the MCP server, but it is a thing that &lt;strong&gt;makes network requests on&lt;br&gt;
your behalf&lt;/strong&gt;. Put a URL in the tool arguments and it will fetch it. The classic&lt;br&gt;
shape of this is asking it for &lt;code&gt;http://169.254.169.254/&lt;/code&gt; — the cloud metadata&lt;br&gt;
service, holding temporary credentials.&lt;/p&gt;

&lt;p&gt;SOIT's egress policy is &lt;strong&gt;deny-by-default&lt;/strong&gt;, in three layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer one: domain policy.&lt;/strong&gt; &lt;code&gt;check_egress_policy&lt;/code&gt; matches the target domain&lt;br&gt;
against tenant-scoped and workspace-scoped allowlists and blocklists, with the&lt;br&gt;
blocklist winning. The defaults are &lt;code&gt;enable_egress_policy: bool = True&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;egress_allowlist: list[str] = []&lt;/code&gt; — an empty allowlist means nothing is&lt;br&gt;
permitted until you say so. And if the policy lookup itself throws, the answer&lt;br&gt;
is deny, not allow:&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="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&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;ForbiddenError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Egress policy lookup failed; request denied&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resource_ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resource_ref&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fail-closed is not a slogan. It is whatever you actually wrote in each &lt;code&gt;except&lt;/code&gt;&lt;br&gt;
branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer two: per-address validation after resolution.&lt;/strong&gt; Passing the domain check&lt;br&gt;
is not enough — DNS rebinding lets an allowlisted hostname resolve to&lt;br&gt;
&lt;code&gt;127.0.0.1&lt;/code&gt; or &lt;code&gt;10.0.0.x&lt;/code&gt;. So after the domain is allowed, &lt;code&gt;GovernedEgressGuard&lt;/code&gt;&lt;br&gt;
actually resolves the hostname and checks&lt;br&gt;
&lt;code&gt;ipaddress.ip_address(address).is_global&lt;/code&gt; for &lt;strong&gt;every&lt;/strong&gt; address returned. One&lt;br&gt;
non-public address and the whole request is refused&lt;br&gt;
(&lt;code&gt;server/app/kernel/security/egress.py&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Closed off in the same pass: non-http/https schemes are denied by default, URLs&lt;br&gt;
carrying userinfo (&lt;code&gt;https://user:pass@host/&lt;/code&gt;) are denied, and a DNS failure is a&lt;br&gt;
denial rather than a retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer three: authorization per hop.&lt;/strong&gt; A URL that cleared both layers returns a&lt;br&gt;
302 pointing at your internal network. Now what? So the outbound HTTPX client is&lt;br&gt;
built like this (&lt;code&gt;server/app/adapters/http/governed_client.py&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize_request&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;httpx&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="o"&gt;-&amp;gt;&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;await&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&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;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;event_hooks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;authorize_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;request_hooks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;kwargs&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;follow_redirects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Authorization hangs off the HTTPX request event hook, so &lt;strong&gt;every request that&lt;br&gt;
actually goes out&lt;/strong&gt; is checked, redirect hops included — not just the URL you&lt;br&gt;
handed in at the entry point. And redirects are not followed by default.&lt;/p&gt;

&lt;p&gt;The MCP adapter builds its sessions with that client, so the whole MCP path —&lt;br&gt;
initialization, &lt;code&gt;list_tools&lt;/code&gt;, every &lt;code&gt;call_tool&lt;/code&gt; — sits inside these constraints.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Can you find out what happened afterwards?
&lt;/h2&gt;

&lt;p&gt;Tool calls are the only place an agent produces real side effects. A model&lt;br&gt;
saying something wrong can be asked again. A tool that changed a row in the&lt;br&gt;
production database changed it.&lt;/p&gt;

&lt;p&gt;SOIT persists each tool call as a step of a run, and writes two pieces of&lt;br&gt;
evidence per call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gateway audit.&lt;/strong&gt; &lt;code&gt;log_gateway_request&lt;/code&gt; with &lt;code&gt;gateway_type="tool"&lt;/code&gt;. The
request side records the &lt;code&gt;tool_ref&lt;/code&gt;, redacted parameters, and the egress
decision (allow / deny plus the target URL). The response side records success,
result type, metadata, and error. &lt;strong&gt;The failure path writes one too&lt;/strong&gt; — the
first thing the &lt;code&gt;except&lt;/code&gt; branch does is emit the audit record. That is the one
people forget, and the one you need when something has gone wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step metrics.&lt;/strong&gt; Latency, success flag, summarized arguments and result,
error code and error details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same call writes a cost entry with &lt;code&gt;billing_basis="requests"&lt;/code&gt;, the provider,&lt;br&gt;
and &lt;code&gt;source_port="tools"&lt;/code&gt; — so "what did this agent's MCP tools cost this month"&lt;br&gt;
is a query you can drill into by agent, workflow, tool, and source&lt;br&gt;
(&lt;code&gt;source_kind=plugin | mcp | builtin&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;On top of that, an OpenTelemetry span &lt;code&gt;soit.tool.invoke&lt;/code&gt; carrying tenant,&lt;br&gt;
workspace, run and step ids, for whatever APM you already run.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Can you replay it?
&lt;/h2&gt;

&lt;p&gt;The most frustrating property of agent debugging is that it does not reproduce.&lt;br&gt;
Same input, different reasoning.&lt;/p&gt;

&lt;p&gt;At the tool layer you can at least be deterministic. Every tool call in SOIT&lt;br&gt;
carries an idempotency key, defaulting to &lt;code&gt;tool:{run_id}:{tool_call_id}&lt;/code&gt;, and&lt;br&gt;
claims a leased execution record. If the claim lands on a record that already&lt;br&gt;
completed, the cached response comes straight back and the external tool is&lt;br&gt;
&lt;strong&gt;not called again&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The retry policy changes accordingly. The comment says it better than I can:&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;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kwargs&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idempotency_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Durable Agent calls are at-most-once at this boundary.&lt;br&gt;
Not every downstream adapter can honor an idempotency key.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At-most-once at this boundary, because you cannot assume the MCP server on the&lt;br&gt;
other end honors your idempotency key. Better to call once too few than once too&lt;br&gt;
many — for writes, that trade is not really a choice.&lt;/p&gt;

&lt;p&gt;Rate limits and daily quotas come along with it, keyed by &lt;code&gt;tool_ref&lt;/code&gt; plus&lt;br&gt;
tenant, workspace and user, so one runaway agent does not burn a whole tenant's&lt;br&gt;
third-party API budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not do
&lt;/h2&gt;

&lt;p&gt;The usual honest list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP transport is streamable HTTP only, targeting the MCP SDK v1 line. The
stateless 2026-07-28 protocol revision is &lt;strong&gt;not supported yet&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;OAuth is &lt;code&gt;client_credentials&lt;/code&gt; only, no authorization_code (see question 2).&lt;/li&gt;
&lt;li&gt;A marketplace for one-click MCP tool installation is on the roadmap; today you
install plugin artifacts by hand.&lt;/li&gt;
&lt;li&gt;The default egress allowlist is empty, which means your first MCP server
&lt;strong&gt;will&lt;/strong&gt; be refused until you add its domain explicitly. That is deliberate,
but it does add a step to the quickstart.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why none of this belongs in the agent framework
&lt;/h2&gt;

&lt;p&gt;A question that comes up constantly: how does this relate to LangChain and&lt;br&gt;
friends?&lt;/p&gt;

&lt;p&gt;They are not the same layer. A framework answers "how do I orchestrate this&lt;br&gt;
call." A runtime answers "under whose identity did this call run, with whose&lt;br&gt;
credentials, what could it reach, what evidence did it leave, and can I replay&lt;br&gt;
it." The first is a concern while you write the code. The second is a concern&lt;br&gt;
after the code ships and someone else asks.&lt;/p&gt;

&lt;p&gt;You can certainly put permission checks inside a framework, but then every new&lt;br&gt;
tool integration reimplements the governance logic. Push it down into the&lt;br&gt;
runtime's port layer and MCP tools, plugin tools and built-in tools all travel&lt;br&gt;
the same path — which is the reason question 1 could say "nobody writes it&lt;br&gt;
twice."&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;SOIT is Apache-2.0 and the code is all on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/soit-ai/soit" rel="noopener noreferrer"&gt;https://github.com/soit-ai/soit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Quickstart: &lt;code&gt;docs/quickstart.md&lt;/code&gt; in the repo&lt;/li&gt;
&lt;li&gt;Governance demo: &lt;code&gt;docs/governance-demo.md&lt;/code&gt; — a 20-minute local script that
walks through permissions, secrets, call audit, cost attribution, replay and
regression, and writes a machine-readable report at the end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are pushing MCP toward production right now, I would genuinely like to&lt;br&gt;
hear which of the five questions is blocking you. In our experience the hardest&lt;br&gt;
one is not technical — it is "who gets to decide what goes on the allowlist."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I maintain SOIT.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
