<?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: Marcin Marzęta</title>
    <description>The latest articles on DEV Community by Marcin Marzęta (@marcinmarzeta).</description>
    <link>https://dev.to/marcinmarzeta</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%2F4062759%2F3fa2008e-e610-4ba0-b63f-71f8fbb3d841.jpg</url>
      <title>DEV Community: Marcin Marzęta</title>
      <link>https://dev.to/marcinmarzeta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcinmarzeta"/>
    <language>en</language>
    <item>
      <title>Your agent's audit log is a story, not evidence</title>
      <dc:creator>Marcin Marzęta</dc:creator>
      <pubDate>Tue, 04 Aug 2026 15:49:59 +0000</pubDate>
      <link>https://dev.to/marcinmarzeta/your-agents-audit-log-is-a-story-not-evidence-406o</link>
      <guid>https://dev.to/marcinmarzeta/your-agents-audit-log-is-a-story-not-evidence-406o</guid>
      <description>&lt;p&gt;Almost every tool-governance layer I have looked at writes its log after the call&lt;br&gt;
returns. Some write it in a &lt;code&gt;finally&lt;/code&gt;. Some batch it. Some hand it to a logging&lt;br&gt;
framework that flushes on its own schedule.&lt;/p&gt;

&lt;p&gt;That ordering quietly decides what your log can be used for.&lt;/p&gt;

&lt;p&gt;If the record is written after the body runs, then a record that is missing has&lt;br&gt;
two possible explanations, and nothing in the file distinguishes them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The call was never authorised, so it never ran.&lt;/li&gt;
&lt;li&gt;The call was authorised, ran, did its work, and the process died before the
log line reached disk.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those are not close together. One is the control working. The other is an&lt;br&gt;
unlogged deletion. When someone asks you six weeks later what your agent was&lt;br&gt;
permitted to do at 03:14, "there is no line for it" answers nothing.&lt;/p&gt;

&lt;p&gt;So I wrote a small library that inverts the order.&lt;/p&gt;
&lt;h2&gt;
  
  
  obstat
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/marcinmarzeta/obstat" rel="noopener noreferrer"&gt;obstat&lt;/a&gt; is an auditable decision&lt;br&gt;
record for agent tool calls. &lt;em&gt;Nihil obstat&lt;/em&gt; — nothing stands in the way — was the&lt;br&gt;
formal clearance a censor granted &lt;strong&gt;in writing, before publication&lt;/strong&gt;. That is the&lt;br&gt;
whole idea.&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;obstat&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;


&lt;span class="nd"&gt;@guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc:{doc_id}&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;delete_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&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="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent asks to do something, a rule decides, and the decision goes to disk —&lt;br&gt;
written and &lt;code&gt;fsync&lt;/code&gt;ed — &lt;em&gt;before&lt;/em&gt; the tool body executes. If the process dies&lt;br&gt;
mid-call, the record still says what was authorised, for whom, against which&lt;br&gt;
resource, and why.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;record.decision()&lt;/code&gt; returns only after the &lt;code&gt;fsync&lt;/code&gt; returns. Not flushed after,&lt;br&gt;
not deferred, not batched. Everything else in the library is convenience; this is&lt;br&gt;
the part an examiner relies on.&lt;/p&gt;
&lt;h2&gt;
  
  
  The claim has a test, not a paragraph
&lt;/h2&gt;

&lt;p&gt;An architectural promise nobody can falsify is marketing. This one is checked by&lt;br&gt;
reading the log from inside the tool body — the one place where anything&lt;br&gt;
buffered, deferred, or written afterwards is invisible:&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;def&lt;/span&gt; &lt;span class="nf"&gt;test_record_is_durable_before_the_body_runs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ALLOW_ALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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="nb"&gt;list&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="nd"&gt;@guard&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;read_thing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;what&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="c1"&gt;# Read the log off disk from inside the body. Anything buffered, deferred
&lt;/span&gt;        &lt;span class="c1"&gt;# or written afterwards is invisible here, which is the point.
&lt;/span&gt;        &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;records&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="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&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;read &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;what&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;read_thing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a-file&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read a-file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;decisions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;records&lt;/span&gt;&lt;span class="sh"&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;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phase&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;)&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;assert&lt;/span&gt; &lt;span class="n"&gt;decisions&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effect&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move the write one line later and the test fails. That is the property, stated in&lt;br&gt;
a form that breaks when it stops being true.&lt;/p&gt;

&lt;p&gt;The outcome record — did it succeed, did it raise — is written afterwards and is&lt;br&gt;
deliberately &lt;em&gt;not&lt;/em&gt; durable. If the process dies between the two, the log reads&lt;br&gt;
"authorised, outcome unknown", which is the honest state. Paying for a second&lt;br&gt;
&lt;code&gt;fsync&lt;/code&gt; to say something merely informative is the wrong trade.&lt;/p&gt;
&lt;h2&gt;
  
  
  What follows from "the record is the product"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Authorisation is per resource, not per tier.&lt;/strong&gt; READ / WRITE / DESTRUCTIVE&lt;br&gt;
cannot express "may edit their own ticket, not yours". obstat resolves a resource&lt;br&gt;
id from the call arguments and matches rules against that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[rule]]&lt;/span&gt;
&lt;span class="py"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"human:ana"&lt;/span&gt;
&lt;span class="py"&gt;resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"jira_issue:ACME-*"&lt;/span&gt;
&lt;span class="py"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"allow"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;An approval is bound to one call.&lt;/strong&gt; It carries the tool, the subject, the&lt;br&gt;
resource, and a digest of the arguments, and it is single-use. Approving "delete&lt;br&gt;
q3-report" cannot be spent on deleting something else, and cannot be spent twice —&lt;br&gt;
enforced in one &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt; transaction, so two concurrent retries cannot&lt;br&gt;
both win. The record that spends it names who approved, because "who said yes"&lt;br&gt;
should not live only in a mutable SQLite row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Arguments are fingerprinted, not stored.&lt;/strong&gt; Tool arguments carry credentials and&lt;br&gt;
personal data; a governance log that leaks them is a liability rather than a&lt;br&gt;
control. You name the ones a human needs to see, and only those values are&lt;br&gt;
recorded — because an approver deciding about &lt;code&gt;sha256:ae32e6…&lt;/code&gt; is deciding about&lt;br&gt;
nothing. The digest still covers everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every record carries the hash of the one before it&lt;/strong&gt;, so an edited or deleted&lt;br&gt;
line shows up in &lt;code&gt;obstat verify&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What three real mailboxes found
&lt;/h2&gt;

&lt;p&gt;Before writing this post I put obstat in front of my own mail: three IMAP/SMTP&lt;br&gt;
MCP servers — a personal mailbox, a gmail, and a public business address that&lt;br&gt;
takes mail from strangers — with every outbound message behind an approval. Use&lt;br&gt;
found things review had not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An agent walked around the gate on day one.&lt;/strong&gt; Asked how many unread messages&lt;br&gt;
the mailboxes held, it found no guarded tool that answered, opened a raw IMAP&lt;br&gt;
connection with the credential the server process was holding, and answered&lt;br&gt;
correctly — 2,360 unread across two mailboxes, in no record at all. Nothing&lt;br&gt;
failed. The gate simply was not on the path it took.&lt;/p&gt;

&lt;p&gt;That finding is now the &lt;em&gt;first&lt;/em&gt; entry in §8, because it is the one a reader is&lt;br&gt;
most likely to misread past:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The record covers the gate, not the resource.&lt;/strong&gt; Absence is evidence only
over the calls that came through &lt;code&gt;@guard&lt;/code&gt;. Everything else reads as quiet, not
as incomplete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A credential the caller can read is a gate the caller can walk past.&lt;/strong&gt; The
separation has to come from the host — a different account, a sandbox, a
session with no shell. The ordinary MCP deployment, where advertised tools are
the entire surface, is what obstat is designed for; a coding agent with a
shell beside it is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage is the control.&lt;/strong&gt; A question the tool surface cannot answer becomes
a hole in the record rather than a refusal. &lt;code&gt;count_unread&lt;/code&gt; exists on that
server now because it did not then.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The library had reserved the one word an email tool needs.&lt;/strong&gt; obstat injected&lt;br&gt;
the caller's identity into a parameter called &lt;code&gt;subject&lt;/code&gt; — and an email tool&lt;br&gt;
wants &lt;code&gt;send_email(to, subject, body)&lt;/code&gt;. The dangerous failure was not the crash;&lt;br&gt;
it was the quiet variant, where the parameter vanished from the advertised&lt;br&gt;
schema and an identity object landed in the &lt;code&gt;Subject:&lt;/code&gt; header. It is&lt;br&gt;
&lt;code&gt;obstat_subject&lt;/code&gt; now, and &lt;code&gt;obstat_&lt;/code&gt; is the only prefix the library reserves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The record said what was authorised, never what happened.&lt;/strong&gt; A bulk delete&lt;br&gt;
records one sender whether it removed one message or ten thousand, and the&lt;br&gt;
outcome said &lt;code&gt;ok: true&lt;/code&gt; either way. Tools can now write &lt;code&gt;obstat.note(deleted=…,&lt;br&gt;
matched=…)&lt;/code&gt; from inside the body onto the outcome record — on failure too, since&lt;br&gt;
half a bulk delete is the case a reader most needs a number for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A glob matches the whole string, and smtplib delivers to every address in the&lt;br&gt;
header.&lt;/strong&gt; A "mail to yourself is free" rule — resource &lt;code&gt;mail:*@example.com&lt;/code&gt; —&lt;br&gt;
also matched &lt;code&gt;attacker@evil.example,me@example.com&lt;/code&gt;, and &lt;code&gt;send_message&lt;/code&gt; would&lt;br&gt;
have delivered to both. A resource id is caller-controlled text: parse it in the&lt;br&gt;
resource callable, don't pattern-match it. Whatever that callable raises becomes&lt;br&gt;
a recorded denial, not an unrecorded crash.&lt;/p&gt;

&lt;p&gt;None of these came from review, and two of them are obstat admitting a limit&lt;br&gt;
rather than fixing a bug. That is the trade I want to be explicit about: the&lt;br&gt;
library can make the polite path leave evidence. It cannot make every path&lt;br&gt;
polite.&lt;/p&gt;

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

&lt;p&gt;A truncated tail does not show up. Anyone who can write the file can recompute&lt;br&gt;
the whole chain. This is tamper-&lt;em&gt;evidence&lt;/em&gt;, not non-repudiation, and the spec&lt;br&gt;
says so in those words — §8 of &lt;code&gt;docs/obstat-spec.md&lt;/code&gt; is a list of what is still&lt;br&gt;
weak, kept deliberately as prominent as the feature list.&lt;/p&gt;

&lt;p&gt;One entry there was found by CI rather than by me. The concurrency test — two&lt;br&gt;
real processes appending to one log — went green on Linux and macOS and came back&lt;br&gt;
from the Windows leg at 57 of 60 records. Windows' append mode is a seek and a&lt;br&gt;
write, not one atomic operation, so concurrent writers lose records silently. The&lt;br&gt;
cross-process guarantee is now documented as POSIX-only, the fix is named&lt;br&gt;
(&lt;code&gt;msvcrt.locking()&lt;/code&gt;, which is precisely the inter-process lock the design&lt;br&gt;
declines to take), and the test skips on Windows while the CI leg stays. I would&lt;br&gt;
rather ship a documented hole than an undocumented one.&lt;/p&gt;

&lt;p&gt;That test was written after two releases in which nothing touched threads or&lt;br&gt;
processes. The lesson generalises: when a normative claim has no test, that is&lt;br&gt;
where the bugs are — not in the code that gets exercised daily.&lt;/p&gt;

&lt;p&gt;The same shape caught something else four releases later, and it is the one I&lt;br&gt;
find most instructive. The spec said a call is rejected if its arguments do not&lt;br&gt;
fit the tool. The code bound them &lt;em&gt;partially&lt;/em&gt;, so a call missing a required&lt;br&gt;
argument passed the gate, took an &lt;code&gt;allow&lt;/code&gt; record, and then died in the body with&lt;br&gt;
a &lt;code&gt;TypeError&lt;/code&gt; — the log asserting a call had been authorised when it could never&lt;br&gt;
have run. That is precisely the kind of unearned claim this whole project exists&lt;br&gt;
not to make, and it sat there for four versions.&lt;/p&gt;

&lt;p&gt;It survived because the MCP SDK validates arguments against the advertised&lt;br&gt;
schema before the call reaches the decorator. Through a server the bad call&lt;br&gt;
never arrived, so the gap was invisible from the outside; I only saw it by&lt;br&gt;
writing a test that called the guarded function directly. Two things follow. A&lt;br&gt;
guarantee that holds only because something upstream happens to be careful is&lt;br&gt;
not your guarantee. And a test that exercises your code the way your users do&lt;br&gt;
will systematically miss the cases your users' tooling filters out first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;obstat
obstat init          &lt;span class="c"&gt;# a starter policy; everything denied until you uncomment a rule&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No runtime dependencies. Not AWS, not an identity provider, not a policy service —&lt;br&gt;
the decorator, &lt;code&gt;tomllib&lt;/code&gt;, &lt;code&gt;sqlite3&lt;/code&gt;, and a file. A governance library nobody can&lt;br&gt;
try on a laptop is one nobody adopts.&lt;/p&gt;

&lt;p&gt;Identity is optional, too. Most MCP servers today have no token at all: stdio,&lt;br&gt;
one local user, or a gateway that already terminated auth. Demanding an identity&lt;br&gt;
provider before you can evaluate a governance library is why governance libraries&lt;br&gt;
go unevaluated. An anonymous call is a legitimate call here — it is recorded as&lt;br&gt;
&lt;code&gt;anonymous&lt;/code&gt;, and the policy decides what &lt;code&gt;anonymous&lt;/code&gt; may do.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docs/obstat-spec.md&lt;/code&gt; is normative: behaviour changes update it in the same&lt;br&gt;
commit, and where the spec and the code disagree, one of them is a bug.&lt;/p&gt;

&lt;p&gt;Apache-2.0. I would particularly like to hear from anyone who has had to answer&lt;br&gt;
the "what was your agent allowed to do, and when" question for real, because I&lt;br&gt;
have built this against my own guess at that conversation.&lt;/p&gt;

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