<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Glen Builds]]></title><description><![CDATA[I build AI systems and write about what I learn. Currently focused on agentic AI, LangGraph, and MLOps.]]></description><link>https://glenbuilds.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 09 Oct 2026 17:10:01 GMT</lastBuildDate><atom:link href="https://glenbuilds.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I Built an AI Agent That Fixes Your AWS Security Issues — Here's Every Technical Decision That Made It Safe]]></title><description><![CDATA[Every AWS account has a graveyard.
A public S3 bucket from a hackathon. An IAM user with AdministratorAccess that was "just temporary." A security group open to 0.0.0.0/0 because debugging was easier ]]></description><link>https://glenbuilds.hashnode.dev/remedi-agentic-aws-security-scanner</link><guid isPermaLink="true">https://glenbuilds.hashnode.dev/remedi-agentic-aws-security-scanner</guid><category><![CDATA[AWS]]></category><category><![CDATA[AI]]></category><category><![CDATA[Python]]></category><category><![CDATA[Devops]]></category><category><![CDATA[cloud security]]></category><dc:creator><![CDATA[Marian Glen Louis]]></dc:creator><pubDate>Tue, 21 Apr 2026 00:52:16 GMT</pubDate><content:encoded><![CDATA[<p>Every AWS account has a graveyard.</p>
<p>A public S3 bucket from a hackathon. An IAM user with <code>AdministratorAccess</code> that was "just temporary." A security group open to <code>0.0.0.0/0</code> because debugging was easier that way. An RDS instance publicly accessible because you were in a hurry and it was 11pm.</p>
<p>The security audit you've been meaning to run has been on the backlog for six months.</p>
<p>I built Remedi to fix that. It's an agentic AI system that scans your AWS account across 8 services, generates a structured findings report, <strong>waits for your approval</strong>, then automatically remediates every vulnerability it found. A verification pass confirms the fixes held.</p>
<p>A full scan costs $0.02 and takes under 5 minutes.</p>
<img src="https://raw.githubusercontent.com/glenlouis8/remedi/main/assets/screenshot-landing.jpeg" alt="Remedi landing page" style="display:block;margin:0 auto" />

<p>This post is a deep dive into every architectural decision that made this trustworthy — not just technically functional. There's one insight in particular I think every engineer building AI agents needs to hear.</p>
<hr />
<h2>Why "Just Let the AI Fix It" Is a Terrible Idea</h2>
<p>My first instinct was the obvious one: AI scans → AI decides → AI fixes → done.</p>
<p>Here's why I didn't build that.</p>
<p><strong>AWS changes are irreversible in practice.</strong> You can delete a security group rule — but you lose the context for why it existed. You can revoke an IAM policy — but you won't know what broke until 3am. You can block public access on an S3 bucket — but if your CDN was reading from it, your site just went dark.</p>
<p><strong>LLMs hallucinate.</strong> Not always. Not obviously. But enough. An LLM making autonomous decisions about which AWS resources to modify will eventually make a confident, catastrophic mistake.</p>
<p><strong>Correct AI decisions can still be wrong for your org.</strong> That IAM user with <code>AdministratorAccess</code>? Maybe it's your CEO's personal account. The AI doesn't know that. You do.</p>
<p>So I built a hard stop into the middle of the pipeline. The AI scans and plans. A human approves. Then — and only then — the AI acts.</p>
<hr />
<h2>The Pipeline: 5 Stages, One Mandatory Human Gate</h2>
<p>Remedi is a LangGraph state machine. Five nodes, one direction, no skipping:</p>
<pre><code class="language-plaintext">Orchestrator → Report Generator → Safety Gate → Remediator → Verifier
</code></pre>
<p>The full stack, layer by layer:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><code>frontend/</code></td>
<td>Next.js 15 dashboard — streams scan output, shows findings, sends approval</td>
</tr>
<tr>
<td><code>server.py</code></td>
<td>FastAPI — Clerk JWT auth, Fernet encryption, spawns the agent as a subprocess</td>
</tr>
<tr>
<td><code>main.py</code></td>
<td>LangGraph pipeline — orchestrator, report generator, safety gate, remediator, verifier</td>
</tr>
<tr>
<td><code>mcp_server/main.py</code></td>
<td>MCP server — all boto3 AWS calls, connected via JSON-RPC over stdio</td>
</tr>
</tbody></table>
<p><strong>Orchestrator</strong> — 8 specialist sub-agents fire in parallel via <code>ThreadPoolExecutor</code>, one per AWS service. Each has its own LLM call loop, dedicated tool set, and structured output format. Results merge into one combined findings message.</p>
<p><strong>Report Generator</strong> — Converts raw findings into a structured remediation plan. Severity levels, resource names, explicit action markers. This is not a summary — it's a machine-readable contract for the next stage.</p>
<p><strong>Safety Gate</strong> — Prints the full report. Stops. Waits for human approval. In CLI mode this is <code>input()</code>. In server mode, the dashboard sends a <code>POST /api/approve</code> that writes <code>"approve\n"</code> to the subprocess stdin. Nothing touches AWS until this unblocks.</p>
<p><strong>Remediator</strong> — Parses the approved report, builds a task list, runs all fixes in parallel. Critically: <strong>no LLM call here</strong>. Pure regex parsing. I'll come back to why.</p>
<p><strong>Verifier</strong> — Re-audits only the resources that were touched. Confirms fixes held. If the remediator failed, it exits immediately rather than producing a misleading clean bill of health.</p>
<hr />
<h2>The Most Important Design Decision: No LLM in the Remediator</h2>
<p>This took the longest to figure out and I think it's the most important thing in this post.</p>
<p>The original design: findings → LLM → tool call → AWS. The LLM decides what to do.</p>
<p>The problem: you've put a probabilistic system in the decision path for irreversible actions. Even with human approval on the overall plan, the LLM can still misread a finding, target the wrong resource, or hallucinate a parameter. Human approval becomes theater.</p>
<p>My solution was to make the report generator produce a strict, machine-parseable format, and make the remediator a deterministic parser rather than an LLM consumer.</p>
<p>Every actionable finding must come out of the report generator in exactly this shape:</p>
<pre><code class="language-plaintext">🔴 [CRITICAL] &lt;resource&gt; is vulnerable -&gt; ACTION: I will call `tool_name`
</code></pre>
<p>The remediator regex-parses that line. Looks up <code>tool_name</code> in an <code>INTENT_MAP</code> that handles a dozen aliases for the same underlying action. Calls the tool directly.</p>
<p><strong>What you lose</strong>: flexibility. New vulnerability types require updating both the prompt and the parser together.</p>
<p><strong>What you gain</strong>: determinism. When you approve the report, you are approving the exact tool calls that will run. No LLM interpretation step between your click and the AWS API.</p>
<p>That's what makes the human gate real. Approval has to mean something specific, or it means nothing.</p>
<hr />
<h2>The Dashboard: What You Actually See</h2>
<p>After connecting your AWS account, you get a live security overview across all 8 services.</p>
<img src="https://raw.githubusercontent.com/glenlouis8/remedi/main/assets/screenshot-dashboard.jpeg" alt="Security overview dashboard" style="display:block;margin:0 auto" />

<p>The scan streams results in real time. Each service reports back as its specialist agent finishes — you're not waiting for all 8 to complete before seeing anything. The frontend reads a <code>StreamingResponse</code> line by line and switches on event prefixes:</p>
<table>
<thead>
<tr>
<th>Prefix</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td><code>[SCAN]</code></td>
<td>JSON event from a specialist agent (updates the service cards live)</td>
</tr>
<tr>
<td><code>[ACTION_REQUIRED]</code></td>
<td>Safety gate reached — approval button appears</td>
</tr>
<tr>
<td><code>[EXEC]</code></td>
<td>Remediator executing a fix</td>
</tr>
<tr>
<td><code>✅</code></td>
<td>Fix confirmed</td>
</tr>
<tr>
<td><code>❌</code></td>
<td>Fix failed</td>
</tr>
</tbody></table>
<p>No WebSockets. No polling. The agent writes to stdout, the server reads and yields it, the browser reads the body stream. Simple enough to debug when a scan stalls mid-stream.</p>
<hr />
<h2>Real Results</h2>
<p>After running multiple scans against a deliberately vulnerable Terraform test environment (8 misconfigured resources), here's what the history tab looks like:</p>
<img src="https://raw.githubusercontent.com/glenlouis8/remedi/main/assets/screenshot-history.jpeg" alt="Scan history and metrics" style="display:block;margin:0 auto" />

<p>100% success rate. 68 seconds average fix time per scan. Every finding verified after remediation.</p>
<p>The test environment provisions exactly the vulnerabilities Remedi targets:</p>
<ul>
<li><p>EC2 with IMDSv2 disabled (SSRF credential theft vector)</p>
</li>
<li><p>S3 bucket with public access enabled</p>
</li>
<li><p>Security group with <code>0.0.0.0/0</code> on port 22</p>
</li>
<li><p>RDS instance publicly accessible</p>
</li>
<li><p>IAM user with <code>AdministratorAccess</code></p>
</li>
<li><p>CloudTrail logging disabled</p>
</li>
<li><p>VPC with no flow logs</p>
</li>
<li><p>Lambda with an overpermissioned execution role</p>
</li>
</ul>
<p><code>terraform apply</code> → run scan → approve → verify all 8 fixed → <code>terraform destroy</code>. That's the integration test loop for every non-trivial code change.</p>
<hr />
<h2>The MCP Async/Sync Bridge Problem</h2>
<p>All boto3 calls live in a separate process — <code>mcp_server/main.py</code> — connected via JSON-RPC over stdio (the Model Context Protocol). The agent talks to it through <code>mcp_client.py</code>.</p>
<p>The problem: LangGraph's <code>ToolNode</code> is synchronous. The MCP client is async. You can't call <code>await</code> inside a sync function.</p>
<p>The fix:</p>
<pre><code class="language-python">_loop = asyncio.new_event_loop()
threading.Thread(target=_loop.run_forever, daemon=True).start()

def _run(coro):
    return asyncio.run_coroutine_threadsafe(coro, _loop).result()
</code></pre>
<p>A background asyncio event loop runs in a daemon thread. Every sync tool wrapper submits its coroutine to that loop and blocks until the result comes back. LangGraph sees a normal synchronous call.</p>
<p>One hard constraint this creates: <strong>no concurrent tool calls</strong>. The MCP server has a single stdio pipe. Two simultaneous JSON-RPC messages corrupt the stream. The parallelism in the orchestrator is at the HTTP level — 8 simultaneous Gemini requests — not at the tool level. Each sub-agent calls tools sequentially within its own loop.</p>
<p>Nothing in the code enforces this. It has to live in the docs and in your head.</p>
<hr />
<h2>Credential Security: Three Independent Expiry Mechanisms</h2>
<p>Remedi is multi-tenant. Multiple users, multiple AWS accounts. Credential storage is not optional security hygiene — it's the foundation.</p>
<p><strong>Fernet encryption at rest.</strong> Access and secret keys are encrypted before hitting PostgreSQL. The encryption key is an environment variable. Database compromise alone gets you ciphertext.</p>
<p><strong>30-minute inactivity purge.</strong> Every credential read updates <code>last_used_at</code>. A background thread runs <code>purge_expired_credentials()</code> every 5 minutes and deletes rows where <code>last_used_at &lt; NOW() - INTERVAL '30 minutes'</code>. Idle credentials don't persist.</p>
<p><strong>Explicit delete on sign-out.</strong> The frontend calls <code>DELETE /api/accounts</code> before completing Clerk sign-out. Credentials are wiped immediately, not left to decay.</p>
<p>One more thing: the IAM user whose keys are in use is auto-detected via STS <code>get_caller_identity</code> and added to <code>PROTECTED_IAM_USERS</code> at scan start. The agent can never remediate itself. Without this, scanning an account with an overprivileged IAM user could revoke its own access mid-run.</p>
<hr />
<h2>Testing Strategy: What You Can and Can't Unit Test</h2>
<p>The agent pipeline is too entangled with LLM outputs and live AWS responses to unit test meaningfully. End-to-end against the Terraform environment is the only test that tells you something true about the pipeline.</p>
<p>But the components underneath the pipeline are different. I have 25 tests, no external services:</p>
<ul>
<li><p><strong>8 tests</strong> for the remediator's regex parser — the single point of failure between human approval and AWS action. Edge cases: multi-finding reports, resource names with dots and hyphens, empty strings, manual-review lines that shouldn't be parsed.</p>
</li>
<li><p><strong>4 tests</strong> for Fernet encryption — round-trips, wrong key, missing key.</p>
</li>
<li><p><strong>13 tests</strong> for the boto3 remediation functions running against <a href="https://github.com/getmoto/moto">moto</a> — an in-memory AWS mock. Real production code. Fake AWS.</p>
</li>
</ul>
<p>The split is deliberate: unit tests for deterministic components, Terraform end-to-end for non-deterministic ones.</p>
<hr />
<h2>What It Costs</h2>
<p>$0.02 per full scan. 8 parallel sub-agents, report generation, verification pass.</p>
<p>The token optimization that gets it there: each pipeline stage receives only the summary output of the previous stage, not the full conversation history. The orchestrator's raw tool-call history never reaches the report generator. The report generator's output never reaches the verifier in full — only the list of resources that were remediated. No stage pays for tokens from two stages back.</p>
<hr />
<h2>The One Thing</h2>
<p><strong>Make human approval mean something.</strong></p>
<p>It's easy to build a "human in the loop" that's actually theater. You show a summary, the user clicks approve, the AI does whatever it was going to do anyway. The approval changes nothing about what runs.</p>
<p>Real human-in-the-loop means: the AI cannot take an irreversible action without sign-off on the exact operations that will execute. Not a vague description. The exact tool, the exact resource, the exact parameters.</p>
<p>In Remedi, the report format enforces this. It's not a summary — it's a specification. When you read <code>I will call \</code>revoke_s3_public_access` on bucket prod-assets` and hit approve, that exact call runs. Nothing else.</p>
<p>Build it any other way and you're not building a safe AI agent. You're building an agent with a "proceed" button.</p>
<hr />
<p><strong>Source:</strong> <a href="https://github.com/glenlouis8/remedi">github.com/glenlouis8/remedi</a></p>
<p><strong>Live:</strong> <a href="https://remedi-kohl-seven.vercel.app/">remedi-kohl-seven.vercel.app</a> — connect an AWS account, run a scan. The CloudFormation template in <code>cloudformation/remedi-agent.yaml</code> creates a least-privilege IAM user with exactly the permissions Remedi needs. Deploy it, copy the keys, delete the stack when done.</p>
<hr />
<h2>About the Author</h2>
<p><strong>Marian Glen Louis</strong> — software engineer based in Buffalo, NY. Building at the intersection of AI and cloud infrastructure.</p>
<ul>
<li><p>Portfolio: <a href="https://glen-louis.vercel.app">glen-louis.vercel.app</a></p>
</li>
<li><p>GitHub: <a href="https://github.com/glenlouis08">github.com/glenlouis08</a></p>
</li>
<li><p>LinkedIn: <a href="https://linkedin.com/in/marian-glen-louis">linkedin.com/in/marian-glen-louis</a></p>
</li>
<li><p>Email: <a href="mailto:glen.louis08@gmail.com">glen.louis08@gmail.com</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[I built an AI that can delete your AWS resources. Here's why that's okay.]]></title><description><![CDATA[Every startup has an AWS account nobody's proud of.
You know the one. Public S3 bucket from a hackathon two years ago. IAM user with AdministratorAccess that someone created "just temporarily." Securi]]></description><link>https://glenbuilds.hashnode.dev/i-built-an-ai-that-can-delete-your-aws-resources-here-s-why-that-s-okay</link><guid isPermaLink="true">https://glenbuilds.hashnode.dev/i-built-an-ai-that-can-delete-your-aws-resources-here-s-why-that-s-okay</guid><dc:creator><![CDATA[Marian Glen Louis]]></dc:creator><pubDate>Sun, 19 Apr 2026 03:54:59 GMT</pubDate><content:encoded><![CDATA[<p>Every startup has an AWS account nobody's proud of.</p>
<p>You know the one. Public S3 bucket from a hackathon two years ago. IAM user with AdministratorAccess that someone created "just temporarily." Security groups open to 0.0.0.0/0 because it was easier to debug that way. RDS publicly accessible because you were moving fast.</p>
<p>The security audit you've been meaning to run? Still on the backlog.</p>
<p>So I built Remedi: an AI system that scans an AWS account across 8 services, generates a findings report, waits for you to say go, then fixes everything it found. Then it re-audits to confirm the fixes held.</p>
<p>This is about the decisions that made it something I'd actually trust.</p>
<hr />
<h3>The problem with "just let the AI fix it"</h3>
<p>My first instinct was the obvious one: AI scans, AI fixes, done. Point an LLM at your AWS account, let it call boto3, ship it.</p>
<p>Here's the thing about AWS changes — they're not really reversible. You can delete a security group rule, but you can't easily recover why it existed. You can revoke an IAM policy, but you won't know what broke until someone calls you at 3am. You can block public access on an S3 bucket, but if your CDN was reading from it, your site just went dark.</p>
<p>LLMs also hallucinate. Not dramatically, not obviously, but enough. An LLM deciding autonomously which resources to touch will eventually make a confident wrong call. In your AWS account, that's not a bug report, it's an incident.</p>
<p>There's also a subtler issue: even a technically correct fix can be wrong for your specific situation. Maybe that IAM user with AdministratorAccess is your CEO's personal account. The AI has no idea. You do.</p>
<p>That's why there's a hard stop in the middle of the pipeline.</p>
<hr />
<h2>The pipeline: 5 stages, one pause</h2>
<p>Remedi is a LangGraph state machine with five nodes:</p>
<p><code>Orchestrator → Report Generator → Safety Gate → Remediator → Verifier</code></p>
<p>The Orchestrator runs 8 specialist sub-agents in parallel via ThreadPoolExecutor, one per AWS service (IAM, S3, EC2, VPC, RDS, Lambda, CloudTrail, Security Groups). Each sub-agent has its own LLM call loop and handles tool errors on its own. Their outputs get merged into one combined findings message.</p>
<p>The Report Generator takes those raw findings and produces a structured report: severity levels, resource names, and action markers. Not a summary — a machine-readable spec for what comes next.</p>
<p>The Safety Gate prints the full report, then stops. Nothing touches AWS until a human explicitly approves. In CLI mode it's just input(). In server mode, the frontend sends a POST to /api/approve, which writes "approve\n" to the subprocess stdin.</p>
<p>The Remediator parses the approved report and executes the fixes. It does not call the LLM again at this point. The parsing is pure regex.</p>
<p>The Verifier re-audits only the resources that were touched. If the remediator failed, it exits rather than producing a misleading clean report.</p>
<p>The whole thing runs as a subprocess. FastAPI spawns main.py and streams stdout line by line to the frontend.</p>
<hr />
<h2>Why the remediator doesn't call the LLM</h2>
<p>This is the decision I spent the most time on.</p>
<p>The obvious design: report generator produces findings → remediator sends them to the LLM → LLM picks a tool → tool runs. Clean, flexible, easy to extend.</p>
<p>The problem: you've put an LLM in the decision path for irreversible AWS changes. Even after a human approves the overall plan, the LLM can still misread a finding, pick the wrong tool, or hallucinate a parameter. The human approved a plan. The LLM is now interpreting it.</p>
<p>So instead, the report generator is required to format every actionable finding like this:</p>
<p><code>🔴 [CRITICAL] is vulnerable -&gt; ACTION: I will call `tool_name</code>`</p>
<p>The remediator regex-parses that string, looks up the tool in an INTENT_MAP (which handles a dozen aliases for the same underlying action), and calls it directly. No LLM. No interpretation step.</p>
<p>You lose flexibility new vulnerability types mean updating both the prompt and the parser together, which is annoying. But when you approve the report, the exact tool calls listed are the ones that run. Nothing happens between your click and the AWS API that you didn't read.</p>
<p>That's the only version of "human approval" that actually means something.</p>
<hr />
<h2>MCP over stdio: the async/sync mess</h2>
<p>All the boto3 calls live in a separate MCP server (mcp_server/main.py) running as a subprocess connected via JSON-RPC over stdio. The agent reaches them through mcp_client.py, which wraps each tool in a synchronous function.</p>
<p>The separation is useful: the MCP server owns AWS, the agent owns the LLM, and the boundary between them is explicit. You could in theory swap out the MCP server without touching the agent.</p>
<p>The awkward part: LangGraph's ToolNode is synchronous. The MCP client is async. You can't await inside a sync context without something holding the event loop.</p>
<p>The fix I landed on is a background asyncio loop in a daemon thread:</p>
<p><code>_loop = asyncio.new_event_loop()</code></p>
<p><code>def _run(coro): return _loop.run_until_complete(coro)</code></p>
<p><code>threading.Thread(target=_loop.run_forever, daemon=True).start()</code></p>
<p>Every tool wrapper submits its coroutine to the background loop, blocks until it finishes, and hands the result back synchronously. LangGraph doesn't know anything async happened.</p>
<p>One constraint this creates, and it's not obvious from reading the code: don't make concurrent tool calls. The MCP server has a single stdio pipe. Two JSON-RPC messages in flight at once will corrupt the stream. The orchestrator's parallelism is at the HTTP level (8 concurrent Gemini calls), not at the tool level. Each sub-agent calls tools one at a time inside its own loop.</p>
<p>Nothing in the code enforces this. It's a comment and a docs note and hopefully you read it.</p>
<hr />
<h2>Credential handling</h2>
<p>Remedi is multi-tenant, so credential storage isn't optional to think about.</p>
<p>AWS access keys are Fernet-encrypted before they hit PostgreSQL. The encryption key lives in an environment variable, separate from the database. If someone gets database access, they get ciphertext.</p>
<p>Credentials expire in a few ways. Every read updates a last_used_at column. A background thread inside the FastAPI process runs purge_expired_credentials() every 5 minutes and deletes rows where last_used_at is more than 30 minutes old. Not a cron job, just a thread. Sign-out is also immediate: the frontend calls DELETE /api/accounts before Clerk's sign-out completes.</p>
<p>One non-obvious thing: whichever IAM user's credentials are in use gets automatically added to PROTECTED_IAM_USERS via STS get_caller_identity at scan start. The agent can't remediate itself. Without this, a scan that flags an overprivileged IAM user could try to revoke the credentials it's currently running under.</p>
<hr />
<h2>Testing without breaking real things</h2>
<p>How do you test an auto-remediator without pointing it at infrastructure you actually care about?</p>
<p>I wrote a Terraform config that deliberately provisions 8 vulnerable resources:</p>
<p>• EC2 with IMDSv2 disabled</p>
<p>• Public S3 bucket</p>
<p>• Security group open to 0.0.0.0/0 on port 22</p>
<p>• Publicly accessible RDS</p>
<p>• IAM user with AdministratorAccess</p>
<p>• CloudTrail disabled</p>
<p>• VPC without flow logs</p>
<p>• Lambda with an overpermissioned role</p>
<p>terraform apply, run a scan, approve, watch all 8 get fixed, terraform destroy. That's the test. There are no unit tests for the agent pipeline — it's too tangled with live LLM outputs and boto3 calls to make mocks useful. End-to-end against real AWS is the only test that tells you something true.</p>
<hr />
<h3>What it costs</h3>
<p>A full scan — 8 parallel sub-agents, report generation, remediation, verification — runs about $0.02 on Gemini 3.0 Flash.</p>
<p>The main reason it's cheap: each pipeline stage gets only the summary output from the previous stage, not the full conversation history. The orchestrator's raw findings get compressed into a structured report before anything downstream sees them. No stage pays for tokens from two stages ago.</p>
<p>Total runtime is 2-4 minutes, mostly network latency to AWS APIs and Gemini.</p>
<hr />
<h3>The thing I'd take away from building this</h3>
<p>"Human in the loop" means different things to different people. A lot of implementations amount to: show the user a summary, let them click approve, the AI does whatever it was going to do anyway. That's a liability disclaimer, not a safety mechanism.</p>
<p>For Remedi, I wanted approval to be binding in a specific way: the report format is a contract. The string "I will call <code>revoke_s3_public_access</code> on bucket prod-assets" isn't a prose description of intent. It's the exact function the remediator will call against that exact bucket. No interpretation after you click approve.</p>
<p>That makes the system rigid and annoying to extend. Every new vulnerability type requires coordinated changes to both the prompt and the parser. I kept second-guessing it. But for something that modifies infrastructure, I'd rather have a system I can fully reason about than one that's flexible in ways I can't predict.</p>
]]></content:encoded></item></channel></rss>