<?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: Ira Rainey</title>
    <description>The latest articles on DEV Community by Ira Rainey (@irarainey).</description>
    <link>https://dev.to/irarainey</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%2F956167%2F30cc2cc5-4709-477e-8b06-c9cfa624cc76.jpeg</url>
      <title>DEV Community: Ira Rainey</title>
      <link>https://dev.to/irarainey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/irarainey"/>
    <language>en</language>
    <item>
      <title>Human-First Engineering</title>
      <dc:creator>Ira Rainey</dc:creator>
      <pubDate>Mon, 20 Apr 2026 15:18:40 +0000</pubDate>
      <link>https://dev.to/irarainey/human-first-engineering-dkh</link>
      <guid>https://dev.to/irarainey/human-first-engineering-dkh</guid>
      <description>&lt;p&gt;There is a question that keeps coming up in conversation at the moment, but one that doesn't seem to have a good clear answer anywhere: &lt;em&gt;&lt;strong&gt;How, as engineers, do we use AI tooling productively and not deskill ourselves in the process — and how do we keep supporting those earlier in their careers?&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My answer has always been that it is more important than ever to understand what &lt;em&gt;good&lt;/em&gt; looks like. Which is fine in principle. It is less useful when someone pushes back and asks what that means in reality, or how you actually apply it when an engineer just raised a pull request with 500 lines of AI-generated code that nobody actually understands.&lt;/p&gt;

&lt;p&gt;That gap is where the idea for &lt;strong&gt;Human-First Engineering&lt;/strong&gt; came from. It is a lightweight manifesto and framework you can adopt today to address these very issues. Everything is published at &lt;a href="https://humanfirstengineering.dev" rel="noopener noreferrer"&gt;humanfirstengineering.dev&lt;/a&gt;, with the source &lt;a href="https://github.com/irarainey/human-first-engineering" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt; under a permissive licence so you can fork it, adapt it, and run with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is it different this time?
&lt;/h2&gt;

&lt;p&gt;As engineers we have long had productivity assistive tooling. Compilers, IDEs, autocomplete, intellisense, static analysis — these all made our lives easier and largely automated the tedious bits. But AI tooling is different. It can automate the parts engineers actually &lt;em&gt;learn from&lt;/em&gt;: reading code, working through a bug, sketching a design, explaining what your code does to another human. Those moments of friction are where the intuition comes from. They are how engineers build the judgement they rely on later.&lt;/p&gt;

&lt;p&gt;Automate those at scale, without thinking about it, and you get a generation of engineers who can ship things but cannot reason about what they shipped or why.&lt;/p&gt;

&lt;p&gt;And that risk is not evenly distributed. It falls most heavily on the engineers entering the industry now. The senior engineers of five years hence are the juniors of today. If AI quietly routes them around the struggle that creates seniors, we are not going to notice the damage until it is too late to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure modes I see
&lt;/h2&gt;

&lt;p&gt;A lot of the conversation about AI-assisted engineering gets framed as "too much AI vs too little." In practice, the real failure modes are subtler.&lt;/p&gt;

&lt;p&gt;The most common one is &lt;strong&gt;using these tools without really understanding what they are generating.&lt;/strong&gt; Engineers accept output because it looks plausible, without the reading and reasoning that would normally catch the issues. The mistakes that show up here are rarely spectacular — they are small, quiet, and compound. Clean architecture; separation of concerns; security; performance; all become invisible in an ever-growing weave of spaghetti code, that is a maintainability timebomb waiting to explode.&lt;/p&gt;

&lt;p&gt;Another issue that gets discussed less than it should: &lt;strong&gt;dependency risk at the individual level.&lt;/strong&gt; I've heard from engineers who lean heavily on an AI tool, hit their token or quota limit mid-cycle, and then spend the rest of the period noticeably less productive than they were before the tool existed, even with more generous enterprise subscriptions. A skill you have outsourced is a skill you don't have when the outsourcing stops.&lt;/p&gt;

&lt;p&gt;Both of these are symptoms of the same thing: treating AI as a productivity multiplier without treating the &lt;em&gt;use of AI&lt;/em&gt; as a skill in its own right. That framing is what this framework is built to address.&lt;/p&gt;

&lt;p&gt;What follows is an attempt to make that framing concrete — something a team can actually pick up and use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the framework?
&lt;/h2&gt;

&lt;p&gt;Human-First Engineering is built on eight beliefs and five core pillars. The beliefs are the &lt;em&gt;why&lt;/em&gt;; the pillars are the &lt;em&gt;how&lt;/em&gt;. The pillars are where the operational content lives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Think first&lt;/strong&gt; — understand the problem before you prompt. AI accelerates execution, not understanding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Own the output&lt;/strong&gt; — every line has a named human owner. If you cannot explain it, you do not ship it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grow through AI, not around it&lt;/strong&gt; — use AI to reach harder problems and understand more deeply, not to avoid the discomfort of not knowing. This is the pillar that protects the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use AI intelligently&lt;/strong&gt; — model choice, context, prompting, and instruction files are skills. Using AI well is part of the modern craft.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust AI, but verify everything&lt;/strong&gt; — calibrate trust to the risk. Human reasoning leads on anything sensitive, irreversible, or security-critical.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you need the whole thing in one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Think first. Own what you ship. Grow through AI, not around it. Use AI intelligently. Verify everything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each pillar has a small set of concrete behaviours. No process. No ceremony. The aim is to be light enough to actually remember, and concrete enough to change the questions asked in a code review.&lt;/p&gt;

&lt;p&gt;The whole thing is deliberately small. You should be able to read the manifesto and framework in ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The toolkit
&lt;/h2&gt;

&lt;p&gt;The framework on its own is useful as a shared mental model, but adoption needs more than beliefs. So alongside it there is a &lt;a href="https://humanfirstengineering.dev/toolkit/" rel="noopener noreferrer"&gt;toolkit&lt;/a&gt; — the bit that turns the manifesto into something a team can actually run.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://humanfirstengineering.dev/toolkit/implementation-guide/" rel="noopener noreferrer"&gt;Implementation Guide&lt;/a&gt;&lt;/strong&gt; — a ten-step rollout plan: introduce the manifesto, then the framework, then embed both into the rituals you already have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://humanfirstengineering.dev/toolkit/practices/" rel="noopener noreferrer"&gt;Practices&lt;/a&gt;&lt;/strong&gt; — concrete patterns for how engineers use AI day to day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://humanfirstengineering.dev/toolkit/slide-deck/" rel="noopener noreferrer"&gt;Slide Deck&lt;/a&gt;&lt;/strong&gt; — a ready-to-present deck for a 30–45 minute team session, editable like code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://humanfirstengineering.dev/toolkit/developer-faq/" rel="noopener noreferrer"&gt;Developer FAQ&lt;/a&gt;&lt;/strong&gt; — the questions engineers actually ask. Honest answers, not corporate ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://humanfirstengineering.dev/toolkit/for-early-career-engineers/" rel="noopener noreferrer"&gt;For Early-Career Engineers&lt;/a&gt;&lt;/strong&gt; — written &lt;em&gt;for&lt;/em&gt; junior engineers, not about them. Practical habits for using AI to grow rather than stall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://humanfirstengineering.dev/toolkit/templates/" rel="noopener noreferrer"&gt;Templates and Prompts&lt;/a&gt;&lt;/strong&gt; — drop-in instruction files for GitHub Copilot and Claude Code, plus reusable prompts for framing, reviewing, and risk-assessing AI-assisted work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to adopt it
&lt;/h2&gt;

&lt;p&gt;Three sensible paths, depending on how much energy you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Just the framework.&lt;/strong&gt; Read the &lt;a href="https://humanfirstengineering.dev/manifesto/" rel="noopener noreferrer"&gt;manifesto&lt;/a&gt; and &lt;a href="https://humanfirstengineering.dev/framework/" rel="noopener noreferrer"&gt;framework&lt;/a&gt;. Share both with your team. Use the one-line summary as a shared vocabulary in code reviews and retros. That alone will shift how people talk about AI-assisted work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework plus a team session.&lt;/strong&gt; Add the &lt;a href="https://humanfirstengineering.dev/toolkit/slide-deck/" rel="noopener noreferrer"&gt;slide deck&lt;/a&gt; and run a 30–45 minute session. Follow up with the &lt;a href="https://humanfirstengineering.dev/toolkit/developer-faq/" rel="noopener noreferrer"&gt;developer FAQ&lt;/a&gt; and the &lt;a href="https://humanfirstengineering.dev/toolkit/for-early-career-engineers/" rel="noopener noreferrer"&gt;early-career guide&lt;/a&gt; as written references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full adoption.&lt;/strong&gt; Run the &lt;a href="https://humanfirstengineering.dev/toolkit/implementation-guide/" rel="noopener noreferrer"&gt;implementation guide&lt;/a&gt; end-to-end. Drop the instruction files into your repositories. Add the prompts to your team's shared prompt library. Set a quarterly review on the calendar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is &lt;a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" rel="noopener noreferrer"&gt;CC BY-NC-SA 4.0&lt;/a&gt;. Fork the repo, cut what does not fit your context, add the examples that will land with your team. The goal is for the principles to be lived, not for the artifacts to be preserved.&lt;/p&gt;

&lt;p&gt;If it is useful to you, take it. If it sparks a better idea, tell me about it — I'd love to hear what you do with it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site: &lt;a href="https://humanfirstengineering.dev" rel="noopener noreferrer"&gt;humanfirstengineering.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/irarainey/human-first-engineering" rel="noopener noreferrer"&gt;github.com/irarainey/human-first-engineering&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>softwareengineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Important: A VS Code Extension That Keeps Your Python Imports Clean</title>
      <dc:creator>Ira Rainey</dc:creator>
      <pubDate>Sat, 07 Mar 2026 14:51:06 +0000</pubDate>
      <link>https://dev.to/irarainey/important-a-vs-code-extension-that-keeps-your-python-imports-clean-je6</link>
      <guid>https://dev.to/irarainey/important-a-vs-code-extension-that-keeps-your-python-imports-clean-je6</guid>
      <description>&lt;p&gt;If you've ever spent time manually cleaning up Python imports after a review comment saying &lt;em&gt;"wrong order"&lt;/em&gt; or &lt;em&gt;"import the module, not the symbol"&lt;/em&gt;, you'll appreciate this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=irarainey.important-python" rel="noopener noreferrer"&gt;Important&lt;/a&gt;&lt;/strong&gt; is a free, open-source VS Code extension that validates and auto-fixes Python import statements according to the &lt;a href="https://google.github.io/styleguide/pyguide.html#313-imports-formatting" rel="noopener noreferrer"&gt;Google Python Style Guide&lt;/a&gt; and &lt;a href="https://peps.python.org/pep-0008/#imports" rel="noopener noreferrer"&gt;PEP 8&lt;/a&gt;. It gives you real-time feedback as you type and can fix every issue it raises with a single command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why another import tool?
&lt;/h2&gt;

&lt;p&gt;Ruff and isort are great at &lt;em&gt;sorting&lt;/em&gt; imports, but they don't enforce the &lt;em&gt;style&lt;/em&gt; rules that teams adopting the Google guide care about — things like importing modules instead of symbols, banning wildcards, or flagging unjustified aliases. Important fills that gap, and its output is designed to be Ruff-compatible so the two tools don't fight each other.&lt;/p&gt;

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

&lt;p&gt;Here's a quick overview of what Important validates and fixes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Auto-Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No relative imports&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;from .module import x&lt;/code&gt; → absolute&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No wildcard imports&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;from os.path import *&lt;/code&gt; → qualified access&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One import per line&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;import os, sys&lt;/code&gt; → separate lines&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import modules, not symbols&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;from PIL import Image&lt;/code&gt; → &lt;code&gt;import PIL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard aliases only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;import numpy as npy&lt;/code&gt; → &lt;code&gt;import numpy as np&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Justified from-aliases only&lt;/td&gt;
&lt;td&gt;Flags pointless &lt;code&gt;as&lt;/code&gt; renames&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unused imports&lt;/td&gt;
&lt;td&gt;Removes dead imports&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate imports&lt;/td&gt;
&lt;td&gt;Merges duplicates&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correct ordering&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;__future__&lt;/code&gt; → stdlib → third-party → first-party → local&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sorted within groups&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;import&lt;/code&gt; before &lt;code&gt;from&lt;/code&gt;, then alphabetical&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Misplaced imports&lt;/td&gt;
&lt;td&gt;Lazy imports inside functions get relocated&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every rule has an auto-fix — no manual intervention required.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it in action
&lt;/h2&gt;

&lt;p&gt;Take this messy import block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;os.path&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models.user&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;abspath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;strong&gt;"Important: Fix Imports in This File"&lt;/strong&gt; (&lt;code&gt;Ctrl+K, Ctrl+Shift+F&lt;/code&gt;) and you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abspath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Imports are split, sorted into groups, wildcard usage is converted to qualified access, and symbol imports are rewritten to module imports — all references in your file are updated automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-time diagnostics
&lt;/h2&gt;

&lt;p&gt;You don't need to run the fix command to see problems. Important validates as you type, highlighting issues directly in the editor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Warnings&lt;/strong&gt; for rule violations (wrong order, wildcards, symbol imports)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faded text&lt;/strong&gt; for unused imports (matching VS Code's convention)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hover info&lt;/strong&gt; explaining exactly what's wrong and why&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each diagnostic links to the relevant style guide rule, so you learn the conventions as you go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart features
&lt;/h2&gt;

&lt;p&gt;A few things that set Important apart from a basic linter:&lt;/p&gt;

&lt;h3&gt;
  
  
  Ruff-compatible output
&lt;/h3&gt;

&lt;p&gt;Sorted imports match Ruff/isort defaults — &lt;code&gt;import&lt;/code&gt; before &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;order-by-type&lt;/code&gt; name sorting (&lt;code&gt;CONSTANT_CASE&lt;/code&gt; → &lt;code&gt;CamelCase&lt;/code&gt; → &lt;code&gt;snake_case&lt;/code&gt;), aliased imports kept on separate lines. You can run both tools without conflicts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-line formatting
&lt;/h3&gt;

&lt;p&gt;Long &lt;code&gt;from&lt;/code&gt; imports are automatically wrapped into Ruff-style parenthesised multi-line format when they exceed the line length:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypackage.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ConfigModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UserModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line length is auto-detected from your &lt;code&gt;pyproject.toml&lt;/code&gt; (&lt;code&gt;[tool.ruff]&lt;/code&gt; → &lt;code&gt;line-length&lt;/code&gt;) or can be set manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;if TYPE_CHECKING&lt;/code&gt; support
&lt;/h3&gt;

&lt;p&gt;Imports inside &lt;code&gt;if TYPE_CHECKING:&lt;/code&gt; blocks are handled correctly — all sorting and validation rules apply within the block, but the &lt;code&gt;import-modules-not-symbols&lt;/code&gt; rule is relaxed since these imports exist purely for type annotations.&lt;/p&gt;

&lt;h3&gt;
  
  
  First-party module detection
&lt;/h3&gt;

&lt;p&gt;Important automatically reads &lt;code&gt;known-first-party&lt;/code&gt; from &lt;code&gt;[tool.ruff.lint.isort]&lt;/code&gt; in your &lt;code&gt;pyproject.toml&lt;/code&gt; and groups those imports correctly between third-party and local. Monorepo support is built in — nested &lt;code&gt;pyproject.toml&lt;/code&gt; files scope their first-party modules to the relevant subtree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inline suppression
&lt;/h3&gt;

&lt;p&gt;Need to keep a specific import the way it is? Add a &lt;code&gt;# noqa: important&lt;/code&gt; comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;os.path&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;join&lt;/span&gt;  &lt;span class="c1"&gt;# noqa: important[import-modules-not-symbols]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use a quick-fix action to add the suppression comment directly from the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;All settings live under &lt;code&gt;important.*&lt;/code&gt; in VS Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"important.validateOnSave"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"important.validateOnType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"important.styleGuide"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"google"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"important.knownFirstParty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"myproject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"important.readFromPyprojectToml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"important.lineLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;lineLength&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt; (the default) auto-detects from &lt;code&gt;pyproject.toml&lt;/code&gt;, falling back to 88.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install it
&lt;/h2&gt;

&lt;p&gt;You can install Important directly from the VS Code Marketplace:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://marketplace.visualstudio.com/items?itemName=irarainey.important-python" rel="noopener noreferrer"&gt;Install Important&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Or search for &lt;strong&gt;"Important"&lt;/strong&gt; in the VS Code Extensions panel (&lt;code&gt;Ctrl+Shift+X&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The source is on &lt;a href="https://github.com/irarainey/important" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; under the MIT licence — contributions and issues welcome.&lt;/p&gt;




&lt;p&gt;If you work on a Python codebase that follows (or wants to follow) the Google Style Guide, give Important a try. It catches the things that linters miss and fixes them without you lifting a finger.&lt;/p&gt;

</description>
      <category>python</category>
      <category>vscode</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
    <item>
      <title>A Consent Dialog Listed 1,467 Partners — So I Used AI to Unmask Them</title>
      <dc:creator>Ira Rainey</dc:creator>
      <pubDate>Sat, 07 Mar 2026 13:28:36 +0000</pubDate>
      <link>https://dev.to/irarainey/a-consent-dialog-listed-1467-partners-so-i-used-ai-to-unmask-them-2jep</link>
      <guid>https://dev.to/irarainey/a-consent-dialog-listed-1467-partners-so-i-used-ai-to-unmask-them-2jep</guid>
      <description>&lt;h2&gt;
  
  
  The Moment That Started It All
&lt;/h2&gt;

&lt;p&gt;Someone sent me a link to an article about a bench on Bristol Live — a local UK news site — and when I clicked on it, a consent dialog popped up. Nothing unusual there. But something made me look closer at the fine print this time. The dialog was asking me to agree to share my data with &lt;strong&gt;1,467 partners&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One thousand, four hundred and sixty-seven. For a story about a bench.&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%2Fmyq6h6y2hfm4e2htwepn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmyq6h6y2hfm4e2htwepn.jpg" alt="Consent Dialog" width="800" height="698"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm not saying that advertising isn't important to allow businesses to generate revenue, but this felt a big much. Curious as to why, I tried to find out more. I clicked and scrolled through the partner list in the dialog, clicking into individual entries, reading purpose descriptions and "legitimate interest" declarations and quickly found myself deep in a rabbit hole. Hundreds of companies I'd never heard of, vague descriptions of data processing purposes, toggles nested inside toggles. After ten minutes I was no closer to understanding what any of these companies actually did with my data, or why a local news article needed nearly fifteen hundred of them. The dialog  was technically giving me information, but in practice it told me almost nothing.&lt;/p&gt;

&lt;p&gt;That's when I thought: there has to be a better way to find out more about what's going on. And the result is an open-source application called &lt;a href="https://github.com/irarainey/meddlingkids" rel="noopener noreferrer"&gt;Meddling Kids&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Illusion of Choice
&lt;/h2&gt;

&lt;p&gt;A recent BBC article titled &lt;a href="https://www.bbc.co.uk/news/articles/c4gj39zk1k0o" rel="noopener noreferrer"&gt;"We have more privacy controls yet less privacy than ever"&lt;/a&gt; hit the nail on the head. We're surrounded by cookie banners, privacy settings, and consent dialogs — yet somehow we end up with less privacy, not more. The article cites Cisco's 2024 Consumer Privacy Survey: 89% of people say they care about data privacy, but only 38% have actually done anything about it.&lt;/p&gt;

&lt;p&gt;And honestly, can you blame the other 62%? The consent mechanism is designed to exhaust you into clicking "Accept". The alternative is scrolling through hundreds of partner names, deciphering purposes written in legalese, and toggling individual switches — all before you can read the article you came for - about a bench. Dr Carissa Veliz, author of &lt;em&gt;Privacy is Power&lt;/em&gt;, put it well: "Mostly, people don't feel like they have control."&lt;/p&gt;

&lt;p&gt;As a software engineer, that felt like an itch I should at least start to scratch. I figured if I could automate the process of visiting a site, accepting its consent dialog, and then capturing exactly what happens behind the scenes — cookies dropped, scripts loaded, network requests fired, storage written — maybe I could pull the mask off what's really going on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter the Meddling Kids
&lt;/h2&gt;

&lt;p&gt;Meddling Kids is a Scooby-Doo inspired privacy analysis tool, because they always unmasked the villain in the end. You give it a URL, it visits the site in a real browser, detects and dismisses the consent dialog, and then captures everything it sees: cookies, scripts, network traffic, localStorage, sessionStorage, and more. It then uses AI to analyse all of that data and produce a privacy report with a deterministic score out of 100.&lt;/p&gt;

&lt;p&gt;The tech stack is a Vue 3 + TypeScript frontend with a Python FastAPI backend. Browser automation is handled by Playwright, running in headed mode on a virtual display (Xvfb) so that ad networks don't block it for being headless. Results stream to the UI in real time via Server-Sent Events.&lt;/p&gt;

&lt;p&gt;But the interesting part is how AI is woven into pretty much every stage of the analysis doing what it is good at - analysing large amounts of data quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI All the Way Down
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Vision Models for Consent Detection
&lt;/h3&gt;

&lt;p&gt;The first challenge is detecting the consent dialog itself. These overlays vary wildly across sites — different consent management platforms, different layouts, different button labels. A brittle CSS selector approach wasn't going to cut it.&lt;/p&gt;

&lt;p&gt;Instead, Meddling Kids takes a screenshot of the loaded page and sends it to a vision-capable LLM. The model looks at the screenshot and identifies whether an overlay is present, what type it is (consent dialog, paywall, sign-in prompt, etc.), and the exact text of the button to click. If the model is confident enough, Playwright clicks that button, and the tool captures a before-and-after comparison.&lt;/p&gt;

&lt;p&gt;There's a fallback chain too: if the vision call times out or can't parse the dialog, a text-only LLM attempt runs against the page content, and if that also fails, a local regex parser takes over. No single point of failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured Analysis with the Microsoft Agent Framework
&lt;/h3&gt;

&lt;p&gt;Under the hood, the analysis pipeline uses the &lt;a href="https://github.com/microsoft/agent-framework" rel="noopener noreferrer"&gt;Microsoft Agent Framework&lt;/a&gt; to orchestrate eight specialised AI agents. Each agent has a focused role — consent extraction, tracking analysis, script classification, cookie explanation, storage analysis, report generation, and summary findings — and they coordinate through a concurrent pipeline with controlled parallelism.&lt;/p&gt;

&lt;p&gt;The structured report agent, for example, generates ten report sections in parallel, while a global semaphore limits concurrent LLM calls to avoid overwhelming the endpoint. Each agent uses structured output with JSON schemas and Pydantic models, so the responses are deterministic and parseable — no fragile prompt-and-pray string parsing.&lt;/p&gt;

&lt;p&gt;The application has the ability to log everything to file so it can be analysed more closely, from operations logs, through Agent Framework threads, to final privacy reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pipeline
&lt;/h3&gt;

&lt;p&gt;The whole analysis runs as a six-phase streaming pipeline over SSE, so results appear in the UI as they happen rather than after a long wait:&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%2Ffzm8e6x2ardrje0htk8a.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffzm8e6x2ardrje0htk8a.jpg" alt="Meddling Kids in action" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Navigation&lt;/strong&gt; — Playwright opens an isolated browser context, navigates to the URL, and waits for the network to settle and content to render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page load and access check&lt;/strong&gt; — Detects bot protection or access denied responses and bails out early if the site blocks us.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initial data capture&lt;/strong&gt; — Snapshots cookies, scripts, network requests, and storage &lt;em&gt;before&lt;/em&gt; any consent interaction. This is the pre-consent baseline — anything captured here was tracking you before you clicked a thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlay handling&lt;/strong&gt; — The vision model detects overlays, Playwright clicks through them, and a consent extraction agent pulls out partner lists, purposes, and CMP details. TC and AC consent strings are decoded and vendor IDs resolved against the IAB Global Vendor List and Google's ATP provider list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent AI analysis&lt;/strong&gt; — Three workstreams run in parallel: script grouping and classification, a structured ten-section privacy report, and a tracking risk analysis. Once the tracking analysis finishes, a summary agent distils everything into prioritised findings. A global semaphore caps concurrent LLM calls at ten to avoid hammering the endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completion&lt;/strong&gt; — The final privacy score, report, and summary stream back to the client.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Making Sense of the Data
&lt;/h3&gt;

&lt;p&gt;A single news site analysis can surface hundreds of cookies, dozens of scripts, and thousands of network requests. No human is going to read through all of that manually, and that's exactly the point — the consent dialogs are counting on it.&lt;/p&gt;

&lt;p&gt;The AI doesn't work in a vacuum though. Bundled with the tool are local databases sourced from public and permissively licensed sources that provide grounding context for the analysis — a form of RAG without a vector store. These include over 19,000 known tracker domains (from Privacy Badger, AdGuard, and EasyPrivacy), nearly 500 script URL patterns, the full IAB Global Vendor List (1,111 TCF vendors), Google's ATP provider list (598 providers), cookie and storage pattern databases, CMP platform signatures, 574 partner risk profiles across eight categories, and media group profiles for 16 UK publishers. This reference data is injected into agent prompts so the LLM can match what it finds against known entities rather than guessing — and it means a large chunk of the classification is deterministic before the model even gets involved.&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%2Fc1kwrz9z8nle0dc43jyf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc1kwrz9z8nle0dc43jyf.jpg" alt="Generated report" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The AI agents summarise what the tracking data actually means in plain language. They surface the risk: which cookies are from data brokers, which scripts are fingerprinting you, which network requests fire before you've even had a chance to consent. The tool also decodes IAB TCF consent strings (those opaque &lt;code&gt;euconsent-v2&lt;/code&gt; values) and Google's Additional Consent strings to show exactly which vendors and purposes are encoded.&lt;/p&gt;

&lt;p&gt;Where possible every cookie, script, and network request is explained and attributed to the company behind it. This makes it very clear what is going on behind the scenes.&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%2Frvdiwxhxtbjodg4abisq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frvdiwxhxtbjodg4abisq.jpg" alt="Network traffic" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Perhaps most usefully for non-technical users, there's a "What You Agreed To" digest — a two to three sentence summary, written at roughly a 12-year-old reading level, explaining what clicking "Accept" actually meant. Something like: "By clicking Accept, you allowed 847 companies to track your browsing activity and share data about you, including with data brokers."&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart Caching to Keep Costs Down
&lt;/h2&gt;

&lt;p&gt;Running vision and language models isn't free, so the tool caches aggressively. Script analysis is cached by script domain, not by the site being scanned — so a Google Ads script analysed on one site is an instant cache hit when the same script appears on another. Overlay dismissal strategies are cached per domain too. In testing against a large news site, a cold run made 72 LLM script calls while subsequent warm runs made zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The whole thing is open source under AGPL-3.0, and you can pull a pre-built Docker image from GitHub Container Registry and have it running in minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3001:3001 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;AZURE_OPENAI_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://your-resource.openai.azure.com/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;AZURE_OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-api-key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;AZURE_OPENAI_DEPLOYMENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-deployment &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/irarainey/meddlingkids:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works with both Azure OpenAI and standard OpenAI — you just need to bring your own model with vision capabilities. I used &lt;code&gt;gpt-5.2-chat&lt;/code&gt; for the main analysis and vision work, and &lt;code&gt;gpt-5.1-codex-mini&lt;/code&gt; for script analysis. Point your browser at &lt;code&gt;http://localhost:3001&lt;/code&gt; and start unmasking.&lt;/p&gt;

&lt;p&gt;If you prefer you can also clone the repo and run it locally with Python and Node in the devcontainer, or build the Docker image yourself using the docker compose file included &lt;code&gt;docker compose up --build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Everything you need to get going — setup, configuration options, Docker Compose, local development — is in the &lt;a href="https://github.com/irarainey/meddlingkids" rel="noopener noreferrer"&gt;README on GitHub&lt;/a&gt;. There is also a comprehensive developer guide explaining how it all works.&lt;/p&gt;

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

&lt;p&gt;Building this tool confirmed what I suspected: the scale of tracking on mainstream websites is genuinely staggering. Some UK news sites drop cookies before you've even interacted with the consent dialog. Scripts from dozens of advertising, analytics, and fingerprinting vendors fire tracking, selling, and sharing your data. Everything from the stories you read, your health and political interests, precise location, and device characteristics. If you're logged into social media on the same device then often this is also automatically shared with them too, driving the algorithms of what appears in your feed. The consent dialog is, in reality, an illusion of control over your data, wrapped in legal form.&lt;/p&gt;

&lt;p&gt;Prof Alan Woodward from Surrey University, quoted in that BBC article, argues that when people assume they're constantly tracked, they self-censor, and that harms free speech and weakens democracy. It's a strong claim, but spend a few minutes watching the tracker graph light up on a typical news site and it starts to feel less academic.&lt;/p&gt;

&lt;p&gt;I don't think the answer is purely technical. There are some great privacy tools out there you should be using, and together with better regulation, better enforcement, and a cultural shift around data privacy all matter more than any tool I can build. But as software engineers, we're in a unique position to make the invisible visible. If nothing else, Meddling Kids lets you see exactly what you're agreeing to — and maybe that's worth knowing before you click "Accept" next time.&lt;/p&gt;

&lt;p&gt;Oh, and that Bristol Post article? When unmasked it scored 100 out of 100.&lt;br&gt;
Zoinks!&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%2F0ompqp01716ql7wmhws2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ompqp01716ql7wmhws2.jpg" alt="Zoinks - 100 score" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;The source code is on GitHub:&lt;br&gt;
&lt;a href="https://github.com/irarainey/meddlingkids" rel="noopener noreferrer"&gt;github.com/irarainey/meddlingkids&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you find it useful, give it a star. And if you run it against your own favourite news site, I'd love to hear what you find.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agentframework</category>
      <category>playwright</category>
      <category>privacy</category>
    </item>
    <item>
      <title>How to Delete Multiple Azure Resources all at Once with Beeching</title>
      <dc:creator>Ira Rainey</dc:creator>
      <pubDate>Mon, 01 May 2023 15:43:18 +0000</pubDate>
      <link>https://dev.to/irarainey/how-to-axe-multiple-azure-resources-all-at-once-1o67</link>
      <guid>https://dev.to/irarainey/how-to-axe-multiple-azure-resources-all-at-once-1o67</guid>
      <description>&lt;p&gt;You know what it's like - you're working on some IaC and spinning up loads of different resources at once, or you've been playing around with ideas and now you're left with a raft of bits and pieces to clean up to save yourself some money, but going into the portal and finding them all can be a hassle, and writing loads of Azure CLI delete statements is a pain. If only there was a tool that offered a single CLI command you could run to delete a bunch of selected stuff at once. Well, now there is. - welcome to Beeching.&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%2Fdikgb9wetrvze3rgjkxq.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%2Fdikgb9wetrvze3rgjkxq.png" alt=" " width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Beeching is a command line tool to help you quickly and easily delete Azure resources you no longer need. Inspired by &lt;a href="https://blog.nationalarchives.gov.uk/the-beeching-axe/" rel="noopener noreferrer"&gt;The Beeching Axe&lt;/a&gt; it allows you to cull vast numbers of resources across a subscription with a single swing of the axe. It can delete multiple resource types at the same time, based on a name, part of a name, or by tag value.&lt;/p&gt;

&lt;p&gt;Resources can be protected from the axe by specifying them in an exclusion list. This allows you to shield resources that you wish to keep. The list of resources can be further restricted to only cull certain types of resource by using another switch.&lt;/p&gt;

&lt;p&gt;Locked resources? No problem, Beeching can cull those too, as long as you have the permission of course.&lt;/p&gt;

&lt;p&gt;Beeching is a cross-platform .NET 6.0 / 7.0 application that can be run on Windows, Linux and Mac. It can be installed via any terminal, or in the Cloud Shell, and can also be run in your CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;You can install the tool globally, using the dotnet tool command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; beeching 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a new version is available you will be prompted when calling Beeching, and you can use the &lt;code&gt;dotnet tool update&lt;/code&gt; command to easily upgrade your installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool update &lt;span class="nt"&gt;--global&lt;/span&gt; beeching 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To call to &lt;code&gt;beeching&lt;/code&gt; and swing the axe, you need to run the command from a user account with permissions to delete the specified resources, and resource locks if using the &lt;code&gt;--force&lt;/code&gt; option. Authentication is performed via Azure CLI. Make sure to run &lt;code&gt;az login&lt;/code&gt; (optionally with the &lt;code&gt;--tenant&lt;/code&gt; parameter) to make sure you have an active session and have the correct subscription selected by using the &lt;code&gt;az account set&lt;/code&gt; command. Alternatively you can specify a different subscription when calling the tool.&lt;/p&gt;

&lt;p&gt;You can invoke the axe using the &lt;code&gt;beeching&lt;/code&gt; command and by specifying your parameters. The most basic usage is to specify the name of the resources you want to axe. This will use your active Azure CLI subscription and will delete all resources that match the name or part of the name. You can use the &lt;code&gt;axe&lt;/code&gt; command, but this is optional as it is the default command so can be omitted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching axe &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiple name values can be supplied in a single string by separating them with the &lt;code&gt;:&lt;/code&gt; symbol as in this example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource-001:my-resource-002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resources can also be selected by tags. This will delete all resources that have a tag with the specified key and value. Tags must be supplied as a single string in the format &lt;code&gt;key:value&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;beeching &lt;span class="nt"&gt;--tag&lt;/span&gt; key:value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Excluding and restricting
&lt;/h2&gt;

&lt;p&gt;Once you have selected the resources you want to axe, you can optionally specify a list of resources to exclude from the axe using the &lt;code&gt;--exclude&lt;/code&gt; option. This allows you to protect resources you wish to keep.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource &lt;span class="nt"&gt;--exclude&lt;/span&gt; my-resource-to-keep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiple name values can again be supplied in a single string by separating them with the &lt;code&gt;:&lt;/code&gt; symbol.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource &lt;span class="nt"&gt;--exclude&lt;/span&gt; keep001:keep002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list of resources can be further restricted to only cull certain types of resource using the &lt;code&gt;--resource-types&lt;/code&gt; option. This example will only axe resources of the type &lt;code&gt;Microsoft.Storage/storageAccounts&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;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource &lt;span class="nt"&gt;--resource-types&lt;/span&gt; Microsoft.Storage/storageAccounts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again multiple options can be specified by single string separating them with a &lt;code&gt;:&lt;/code&gt; symbol, as shown in this example which will axe only storage accounts and virtual networks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource &lt;span class="nt"&gt;--resource-types&lt;/span&gt; Microsoft.Storage/storageAccounts:Microsoft.Network/virtualNetworks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resource Groups
&lt;/h2&gt;

&lt;p&gt;By default the axe will only cull individual resource types. If you want to axe an entire resource group and all the resources within it, you can use the &lt;code&gt;--resource-group&lt;/code&gt; option. This will axe the resource group and all resources in it. This option can be used with the &lt;code&gt;--name&lt;/code&gt; or &lt;code&gt;--tag&lt;/code&gt; options to axe resource group that match the name, or partial name, or tag key and value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource-group &lt;span class="nt"&gt;--resource-group&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of these options can be combined to create a very specific axe that will only delete the resources you want to delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource Locks
&lt;/h2&gt;

&lt;p&gt;Resource locks can be applied to Azure resources at the resource, resource group or subscription level. If a resource is locked, it cannot be axed. Beeching will check for resource locks and will not attempt to axe any resources that are locked. If you want to axe a resource that is locked, you will need to remove all applicable locks first, or use the &lt;code&gt;--force&lt;/code&gt; option to override the locks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the &lt;code&gt;--force&lt;/code&gt; option will attempt to remove any resource locks before axing the resource. This can be useful if you have a resource that is locked, but you know that it is safe to delete. This option should be used with caution as it can lead to accidental deletion of resources.&lt;/p&gt;

&lt;p&gt;Following the axing of a locked resource, any relevant locks, such as subscription locks or resource group locks will be reapplied. This is to prevent accidental deletion of resources that are locked for a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  What If?
&lt;/h2&gt;

&lt;p&gt;It is also possible to use the &lt;code&gt;--what-if&lt;/code&gt; parameter to see which resources would face the axe. This will show you the list of resources that would be deleted, but will not actually delete anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource &lt;span class="nt"&gt;--what-if&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Confirmation
&lt;/h2&gt;

&lt;p&gt;Before any resources are actually deleted, you will be prompted to confirm that you really want to delete the resources. For automated deletion such as in a CI/CD pipeline you can skip this prompt by using the &lt;code&gt;--yes&lt;/code&gt; parameter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Retries
&lt;/h2&gt;

&lt;p&gt;A built-in retry mechanism is in place to handle transient network errors. By default, the axe will retry each request 3 times at the API level.&lt;/p&gt;

&lt;p&gt;Occasionally deletion requests can fail if other dependent resources have yet to be deleted. In this instance a further retry mechanism is in place which will pause for 10 seconds between each retry attempt, and each action will be retried 6 times. These two values are configurable and can be set using the &lt;code&gt;--max-retry&lt;/code&gt; and &lt;code&gt;--retry-pause&lt;/code&gt; parameters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beeching &lt;span class="nt"&gt;--name&lt;/span&gt; my-resource &lt;span class="nt"&gt;--max-retry&lt;/span&gt; 10 &lt;span class="nt"&gt;--retry-pause&lt;/span&gt; 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beeching can be found on &lt;a href="https://www.nuget.org/packages/beeching/" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt; and &lt;a href="https://github.com/irarainey/beeching" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cli</category>
      <category>developer</category>
      <category>tools</category>
    </item>
    <item>
      <title>Manage Azure App Registrations in VS Code</title>
      <dc:creator>Ira Rainey</dc:creator>
      <pubDate>Tue, 10 Jan 2023 09:59:52 +0000</pubDate>
      <link>https://dev.to/irarainey/manage-azure-app-registrations-in-vs-code-lpm</link>
      <guid>https://dev.to/irarainey/manage-azure-app-registrations-in-vs-code-lpm</guid>
      <description>&lt;p&gt;If you work with Microsoft Azure and use Application Registrations to create OAuth2 scopes for your APIs, or maybe create client applications that consume scopes from other applications, then you will no doubt be managing this all in the Azure Portal. That's great, and most of the functionality is there.&lt;/p&gt;

&lt;p&gt;If you work with large numbers of applications, or if you automate parts of your works then you might perform these tasks with PowerShell or even by calling the Microsoft Graph API directly to manage these Azure Active Directory objects.&lt;/p&gt;

&lt;p&gt;But if you're working in Visual Studio Code building your application, wouldn't it be great to be have all of your Application Registration information and management to hand directly in Visual Studio Code? Well now you can.&lt;/p&gt;

&lt;p&gt;As a side project I have built a new extension for Visual Studio Code to allow for the management of Application Registrations from within the IDE.&lt;/p&gt;

&lt;p&gt;It allows for easy viewing, copying, adding, and editing of most the core application properties, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client Id&lt;/li&gt;
&lt;li&gt;Application ID URI&lt;/li&gt;
&lt;li&gt;Sign In Audience&lt;/li&gt;
&lt;li&gt;Certificates and Secrets&lt;/li&gt;
&lt;li&gt;Redirect URIs&lt;/li&gt;
&lt;li&gt;API Permissions&lt;/li&gt;
&lt;li&gt;Exposed API Permissions&lt;/li&gt;
&lt;li&gt;App Roles&lt;/li&gt;
&lt;li&gt;Owners&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also allows for the simple creation of new applications, quickly viewing of the full application manifest in the editor, and has the ability to open the application registration directly in the Azure Portal when you need full editing control.&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%2F8feq77g4xbqnz2pz7kje.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%2F8feq77g4xbqnz2pz7kje.png" alt="Visual Studio Code" width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All application properties have their own range of functionality. From the top-level application itself, down to each individual property, functionality can be accessed via a range of context menus.&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%2Fr1w463izk2b5ou3n12br.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%2Fr1w463izk2b5ou3n12br.png" alt="Context Menus" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp12vcps30paxtlbab0pu.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%2Fp12vcps30paxtlbab0pu.png" alt="Context Menus" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If required functionality is not currently implemented for a particular property then you can open the application registration in the Azure portal from the context menu of the application itself.&lt;/p&gt;

&lt;p&gt;You can install the extension directly from with Visual Studio Code, or you can find out more information about it by visiting the &lt;a href="https://marketplace.visualstudio.com/items?itemName=irarainey.applicationregistrations" rel="noopener noreferrer"&gt;Visual Studio Code Marketplace&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Using Playwright to test Azure Active Directory secured APIs</title>
      <dc:creator>Ira Rainey</dc:creator>
      <pubDate>Mon, 28 Nov 2022 11:57:01 +0000</pubDate>
      <link>https://dev.to/irarainey/using-playwright-to-test-azure-active-directory-secured-apis-38p5</link>
      <guid>https://dev.to/irarainey/using-playwright-to-test-azure-active-directory-secured-apis-38p5</guid>
      <description>&lt;p&gt;So you've built yourself a great API, and because you're smart you've secured it using JSON Web Tokens that you need to get from Azure Active Directory. Nice. As it's a user-focussed API you've implemented an authorisation model based on the user scopes defined in the access token. Smart.&lt;/p&gt;

&lt;p&gt;However, now you want to perform end-to-end testing of your API endpoints, but you've suddenly realised that you need a user to authenticate themselves with Azure Active Directory to generate the access token you need.&lt;/p&gt;

&lt;p&gt;If you were using the &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow" rel="noopener noreferrer"&gt;Client Credentials OAuth2 grant type&lt;/a&gt; you wouldn't have this problem, but that would only allow you to use application permissions and not user delegated scopes as you need. So using the &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow" rel="noopener noreferrer"&gt;Authorization Code OAuth2 grant type&lt;/a&gt; you're now presented with the challenge of automating that authentication process.&lt;/p&gt;

&lt;p&gt;One way of doing this would be to use the &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc" rel="noopener noreferrer"&gt;Resource Owner Password Credentials (ROPC) OAuth2 grant type&lt;/a&gt;, which allows you to simply pass the username and password with the token request and be given back an access token. But this grant type is considered insecure and should be avoided if possible. It is also proposed to be omitted from the &lt;a href="https://oauth.net/2.1/" rel="noopener noreferrer"&gt;OAuth2.1 standard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A better way to achieve this would be to use the open source browser automation framework &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt;, which offers the ability to test applications in Chromium, Firefox and WebKit with a single API, using .NET, Python, Java, or Node.js.&lt;/p&gt;

&lt;p&gt;While Playwright is a fully-fledged automation framework for testing browser-based applications, in our scenario we're only interested in automating the Azure Active Directory authentication process. This will enable us to obtain an access token with user-scoped claims to allow us to test our API authorisation model.&lt;/p&gt;

&lt;p&gt;This example uses a .NET 6.0 console application (a Node version can be found in the comments) with Playwright and the &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows" rel="noopener noreferrer"&gt;Microsoft Authentication Library&lt;/a&gt; (MSAL). The token acquisition process uses the Authorization Code grant type which following successful authentication of a user will return an authorization code to the specified redirect URI. This is the part that will be automated using Playwright. The returned authorization code is then exchanged for an access token using MSAL.&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%2Fesgdva01ixmto9a95rbi.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%2Fesgdva01ixmto9a95rbi.png" alt="Authorization Code Grant Type" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A client &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app" rel="noopener noreferrer"&gt;Application Registration&lt;/a&gt; is required to be registered in your Azure Active Directory tenant, with all required user permissions consented, and a redirect URI configured to receive the authorization code. This example uses &lt;a href="https://oidcdebugger.com/" rel="noopener noreferrer"&gt;https://oidcdebugger.com/&lt;/a&gt; which is then accessed by Playwright to retrieve the code. This could however just as easily be your own service.&lt;/p&gt;

&lt;p&gt;Your tenant id, client id, and scope are required for the sample to function. In the snippet below they are shown as placeholders. In the GitHub repository these values are set from either an &lt;code&gt;appsettings.json&lt;/code&gt; or &lt;code&gt;usersecrets.json&lt;/code&gt; file. This is only designed as an example. If you are running this anywhere other than locally it's recommended to store these values in &lt;a href="https://learn.microsoft.com/en-us/azure/key-vault/secrets/about-secrets" rel="noopener noreferrer"&gt;Azure Key Vault secrets&lt;/a&gt; and access them using &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview" rel="noopener noreferrer"&gt;Managed Identity&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get client app related related settings&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt; AAD TENANT ID &amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt; AAD CLIENT ID &amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt; API SCOPE &amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The redirect uri being used here could be any service that you can use to access the auth code&lt;/span&gt;
&lt;span class="c1"&gt;// after it has been redirected from Azure Active Directory.&lt;/span&gt;
&lt;span class="c1"&gt;// This is using https://oidcdebugger.com which of course determines how you extract the auth code&lt;/span&gt;
&lt;span class="c1"&gt;// from page at the step lower down to use to exchange for an access token&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;redirect_uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://oidcdebugger.com/debug"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Define authority and login uri&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"https://login.microsoftonline.com/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;login_uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/oauth2/v2.0/authorize?client_id=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;redirect_uri=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;scope=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;response_type=code&amp;amp;prompt=login"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create a Playwright instance&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Creating Playwright instance"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Playwright&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Launch an instance of Chrome&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Launching Chrome"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LaunchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BrowserTypeLaunchOptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Headless&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Create a browser page&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewPageAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Navigate to the login screen&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Navigating to &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;login_uri&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GotoAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_uri&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Enter username&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Entering username"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FillAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input[name='loginfmt']"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt; USERNAME &amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClickAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input[type=submit]"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Wait until page has changed and is loaded&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitForLoadStateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoadState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NetworkIdle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Enter password&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Entering password"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FillAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input[name='passwd']"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt; PASSWORD &amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClickAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input[type=submit]"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Wait until page has changed and is loaded&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitForLoadStateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoadState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NetworkIdle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Extract the auth code from the page we've redirected it to&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Extract auth code"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;authCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InnerTextAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#debug-view-component &amp;gt; div.debug__callback-header &amp;gt; div:nth-child(4) &amp;gt; p"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Close the browser&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Build an MSAL confidential client&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConfidentialClientApplicationBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithAuthority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithRedirectUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithClientSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt; CLIENT SECRET &amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Get access token with code exchange&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Request access token with MSAL"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;AuthenticationResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AcquireTokenByAuthorizationCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;authCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Display the access token from the response&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Access token retrieved:\n"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have exchanged your authorization code for an access token, you are free to use that token to call your API endpoints passing the token in the &lt;code&gt;Authorization&lt;/code&gt; header as usual.&lt;/p&gt;

&lt;p&gt;It is worth noting that this example will not work if multi-factor authentication is enabled for the user to be authenticated. It is recommended to create test users in your tenant that are excluded from MFA using a &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/conditional-access/overview" rel="noopener noreferrer"&gt;Conditional Access Policy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If that is not possible, then you could configure MFA to use an SMS number and use a service such as &lt;a href="https://www.twilio.com/" rel="noopener noreferrer"&gt;Twilio&lt;/a&gt; to trigger a webhook from incoming SMS messages, which could then be included into the automated token acquisition process. This is not included in the scope of this article.&lt;/p&gt;

&lt;p&gt;Full Code: &lt;a href="https://github.com/irarainey/PlaywrightTokenAcquisition" rel="noopener noreferrer"&gt;https://github.com/irarainey/PlaywrightTokenAcquisition&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tooling</category>
    </item>
  </channel>
</rss>
