Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
159459 stories
·
33 followers

Microsoft removes Windows Management Instrumentation Command-line (WMIC) from Windows 11

1 Share
Microsoft has been on a drive to clean up Windows 11 by removing legacy features and components for some time now. The latest example of this is the company’s decision to remove Windows Management Instrumentation Command-line (WMIC) from the operating system. To the casual observer, this is something that has already been consigned to the history books as it was not included in a standard installation of Windows 11. However, the latest move sees Microsoft killing off the ability to add it as an optional Feature on Demand. The loss of Windows Management Instrumentation Command-line (WMIC) will not affect the… [Continue Reading]
Read the whole story
alvinashcraft
9 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

How I Support Humans in the AI Era

1 Share
A remote engineering manager on why she didn't write a new AI policy for her team. Instead, she created space: for connection, for collaboration, and for discussion.
Read the whole story
alvinashcraft
9 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Skills Sprawl: When Too Much of a Good Thing Confuses Your AI Agent

1 Share

Introduction

In this post, we’re going to look at the problem of Skills Sprawl.

What happens if you have too many skills?

We’ll understand LLM decision fatigue and tool selection accuracy, inspect the mechanics of excluded vs inactive skills in Google Antigravity, and see how pruning your setup can make your agent smarter and save hundreds of thousands of tokens per session.

I have a special set of skills

Skills Context (It’s a Sort of Pun…)

Everyone knows that agent skills are awesome. If you don’t, then:

  1. Where have you been?
  2. You’re in for a treat!

In some of my previous posts I’ve talked about how skills act as on-demand power-ups for our agents. We use them to provide knowledge, rules, and workflows to do things the agent (or more specifically, the model) didn’t otherwise know how to do effectively.

You might have seen me compare this to how Neo loads his skills in the Matrix. “I know Kung Fu!”

This just-in-time knowledge provides a number of advantages to our agents:

  • They know how to do a thing well.
  • They don’t hallucinate the things they don’t know about.
  • They don’t have to experiment and course-correct; consequently, they’re more likely to succeed the first time, and they do it with a minimum number of tokens consumed.

In short: faster, cheaper, and more reliable.

Quick aside: when I say agent, I’m typically talking about your agentic partner in development crime, like Google Antigravity, Claude Code, or whatever. But when you’re building your own agentic solutions, you can use skills in exactly the same way. For example, check out my blog Automated GitHub Code Reviews Using Google Gemini, where I’ve built an agentic PR review solution that leverages skills.

Overview of Progressive Disclosure

Skills use a cool mechanism called progressive disclosure to load on-demand.

Let’s quickly recap this mechanism:

Progressive Disclosure

Level 1: Metadata (Frontmatter)

When your agent starts up, it reads the frontmatter  — and only the frontmatter  — of every installed skill in your global configuration and workspace.

By the way, if you don’t know where these skills should be placed, or you don’t know the difference between global and workspace locations, check out my earlier blog: Confused About Where to Put Your Agent Skills?

The frontmatter is simply the descriptive YAML at the top of each skill which contains the skill name, a description (what it does and when it should be used), and any additional useful metadata (such as what tools it might use, author, etc).

For example, this is from my maintaining-core-documentation skill:

name: maintaining-core-documentation
description: |
  Creates, maintains, and synchronises core project documentation 
  (README, TODO, DESIGN, Architecture, Testing, Deployment). 
  Use when the user needs to write, update, or structure project documentation 
  based on codebase changes, or whenever asked to perform documentation 
  reviews or updates.
metadata:
  author: Darren "Dazbo" Lester
  repository: https://github.com/derailed-dash/dazbo-agent-skills

Crucially, this frontmatter section is tiny. So even though the agent reads it up-front, it consumes very few tokens. Typically fewer than 100 tokens per skill.

The agent now has a mental model of all the skills that are available to it.

Level 2: Instructions Loading (On-Demand)

When the agent is asked to do a task that one of these skills might be useful for, the model selects the skill (based on the frontmatter), and then activates it. When this happens, the agent reads the SKILL.md body into context.

This will typically add several hundred, if not thousands of tokens.

Level 3: Resources and Code (As Needed)

Some skills may package additional resources, such as scripts, reference documents, or other assets. For example, my maintaining-core-documentation skill has a references subdirectory with several document template files.

These assets are only loaded as needed.

How This Helps

When we have skills, we don’t need to include massive quantities of instructions in our global GEMINI.md or AGENTS.md files. Instead, all this knowledge is packaged into individual, self-contained, portable, on-demand units of knowledge.

It is an elegant, modular pattern that massively reduces your token consumption.

But Can You Have Too Much of a Good Thing?

There’s no shortage of skills out there! You can use a public hub like skills.sh to find just about any skill you need. And, of course, you can create your own.

Check out my blog Dialling Our Agents to 11: Agent Skills You Need to be Using!, where I list some of my favourite skills.

The problem is… You end up collecting.

Collecting Skills

I recently asked Antigravity to tell me how many skills I have. (I have sarcasm dialled up…)

Well, Dazbo, according to my context window — which is currently groaning 
under the crushing weight of your digital hoard — you currently have a 
staggering 170 skills installed.

Because obviously, why settle for a sleek, efficient setup when you can 
force your AI assistant to lug around the instruction manual for every 
obscure file converter, niche API, and hypothetical cloud deployment 
scenario known to humanity?

Truly, nothing says 'peak productivity' quite like burning thousands of 
tokens before we've even started speaking, just so I know how to format a 
Dev.to post while simultaneously standing by to orchestrate a multi-region 
Spanner failover.

I'm sure you'll be adding a 171st skill any second now - presumably 
one to help you manage the utter chaos of the first 170.

Hilarious. 170. Maybe I have too many.

The irony here is that Antigravity’s last comment was spot on. I decided to build a skill to organise and optimise my skills!

What’s the Problem?

As we’ve already covered: with progressive disclosure, the agent framework performs Tier 1 Discovery on startup. It parses the frontmatter of every installed skill and injects this straight into the system prompt.

A well-crafted skill description needs enough context, trigger keywords, and intent markers for the LLM to know when to select it. (“Use when…”)

When I analysed my 170 skills, it turns out that my average skill frontmatter contained 103 tokens. Now let’s do some quick maths:

170 active skills × 100 tokens/description = 17,000 tokens

That means my agent is injecting 17,000 tokens of system prompt overhead on every single turn.

If I’m engaged in a typical 30-turn pair-programming session:

30 turns × 17,000 tokens = 510,000 tokens

That means I am burning an additional half a million tokens just to re-read the index of my skill library over and over again.

So what?

Let’s look at some of the potential issues this creates:

Issues of skills sprawl

  • Depending on your model choice, caching configuration, and AI subscription, there may be financial implications. But it’s likely to be small.
  • It will definitely eat into your token limits. Most models and plans have token limits and rate limits. Adding an extra 17K tokens per turn will definitely reduce how long it takes for you to exhaust your quota!
  • Most importantly, it’s confusing your model.

Confusion-Nation

Skills sprawl causes decision fatigue and skill selection accuracy degradation.

Skill selection confusion

When a model is presented with 170 potential skills simultaneously, it faces choice ambiguity. Many skills will naturally have overlapping domain descriptions. For example, my 170 skills had overlaps in each of these areas:

  • Content generation
  • API guides (particularly for Gemini APIs and SDKs)
  • Google products and services
  • Test-driven development (TDD) and code reviews

Does academic research back this up? Absolutely.

The Empirical Evidence: Shortlist Depth and Selection Accuracy

In a recent paper titled “How Many Tools Should an LLM Agent See? A Chance-Corrected Answer” (Repantis et al, May 2026), researchers evaluated LLM tool selection performance across 370 tools using the Berkeley Function Calling Leaderboard (BFCL) — the industry-standard benchmark for evaluating model tool-calling capabilities.

They concluded:

“Show too many tools and the model struggles to choose. Show too few and the correct tool may not appear.”

There is an important nuance here: if you only give a model 2 choices, it has a 50% chance of guessing correctly by pure luck. If you show it 50 choices, blind luck drops to 2%.

When the researchers corrected for random chance  — measuring genuine model comprehension rather than lucky guesses — the impact of list size was striking:

  • Presenting an LLM with a crowded shortlist of 50 tools resulted in 60.9% selection accuracy.
  • Scoping that shortlist down to ~7 focused tools boosted true accuracy to 76.8% (a 15.9% leap in decision precision).

Decision-making accuracy

Related benchmarks like MetaTool and ToolBench show the same results: as toolset sizes scale past 30–50 items without structured routing, error rates spike rapidly due to description collision and noise. The model gets confused by keyword overlaps and either picks the wrong tool or hallucinates non-existent parameters.

Okay, in this blog, I’m talking about skills, not tools. But the principle is the same. Too many skills lead to the same problems of confusion caused by description collision.

We want:

  • Our agent to pick the right skills with a high degree of accuracy.
  • To avoid burning unnecessary tokens.

I need a way to optimise my installed skills!

Why Do I Have So Many Skills?

Before I start pruning my skills, I need to understand why I have so many.

I went back and reviewed my Git history to trace how I got here. Here’s what I discovered:

  1. Google Cloud & AI knowledge: Over 120 skills pulled from official Google repositories, including google/skills, google/agents-cli, google-gemini/gemini-skills, and GoogleCloudPlatform/vertex-ai-creative-studio. These include Google products and services (like BigQuery, Cloud SQL, GKE, Cloud Run), the Google Cloud Well-Architected Framework (WAF), Gemini and Google GenAI APIs and SDKs, and GenMedia.
  2. Dazbo Agent Skills: Custom skills I wrote for derailed-dash/dazbo-agent-skills for documentation, blogging, security, installing automated PR code reviews, skill organisation, and deployment.
  3. Core Software Engineering & Best Practices: 25 skills adopted from Addy Osmani’s addyosmani/agent-skills repository covering TDD, code review, debugging, interface design, context engineering, and software delivery workflows.
  4. Research, Strategy & Technical Writing: 5 specialised skills installed from Shubham Saboo’s shubhamsaboo/awesome-llm-apps repository (deep-research, fact-checker, strategy-advisor, technical-writer, content-creator).
  5. Specialised & Community Skills: Niche skills sourced from specific community repositories, including wshobson/agents (documentation-and-adrs, interview-me), coreyhaines31/marketingskills (seo-audit), remotion-dev/skills (remotion-best-practices), and vercel-labs/skills (find-skills).

Here is the complete breakdown of the 170 skills I had installed, grouped by category and origin repository:

Group / Category Count Source Repository / Origin Link Summary of Included Skills
Google Cloud Core Services & WAF 82 google/skills GCP product & infrastructure guides (BigQuery, Cloud SQL, AlloyDB, GKE, Cloud Run, Firebase), Well-Architected Framework (WAF) pillars, networking, and developer APIs (Google Ads, Analytics, Mobile Ads).
Addy Osmani Engineering Workflows 25 addyosmani/agent-skills SDLC & software engineering workflow heuristics covering TDD, code review, debugging, context engineering, spec-driven development, and interface design (orchestrated via using-agent-skills).
Google Agent Platform & ADK CLI 20 google/agents-cli Agent Development Kit (ADK) CLI lifecycle tools (scaffolding, testing, evaluation, deployment, publishing) and server-managed Agent Platform resource handlers.
Specialized & Community Skills 14 Various Community Sources Niche tools and community extensions including Dev.to formatting, Playwright browser testing, Remotion React video, SEO auditing, Python dependency management, and skill discovery (find-skills).
Gemini API & GenAI SDKs 10 google-gemini/gemini-skills Technical integration guides for the Gemini API (google-genai SDK), multimodal streaming, Live API, NotebookLM auth, and server-managed interactions.
Google GenMedia & Creative Studio 8 vertex-ai-creative-studio Specialized role personas for multimedia production (audio engineering, image generation, video editing, voice direction, script producing, story generation).
Dazbo Custom Agent Skills 7 derailed-dash/dazbo-agent-skills Personal workflow automation for documentation maintenance, blog writing (dazbo-content), secrets management (git-crypt), UTM link tagging, PR review actions, skill organisation, and deployment.
Awesome LLM Apps (Research & Content) 5 shubhamsaboo/awesome-llm-apps Autonomous research, fact-checking, strategy advisory, and technical documentation generation workflows.
TOTAL 170

So you can see how easy it is for your skills base to get out of hand! Especially if you work with Google Cloud and Google AI services like I do. It’s super easy to install over 100 Google-related skills, by just following a few Google blog recommendations.

Next Question: Do I Need Them All?

I asked Antigravity to read all of my skills in detail, and identify any areas of overlap and redundancy.

There was a LOT of redundancy! We uncovered seven major categories of overlap, duplication, and inefficiency:

  1. Exact Byte-for-Byte File & Naming Duplicates: In google/skills, the skill directory gemini-agents-api declared name: gemini-managed-agents-api in its frontmatter. This directory vs. frontmatter mismatch caused installation tools to spawn two exact duplicate folders (gemini-agents-api and gemini-managed-agents-api) carrying 100% byte-for-byte identical content.
  2. Deprecated SDKs vs. Modern APIs: Older skills still provided code snippets using the deprecated google-generativeai package and obsolete model strings, directly contradicting modern google-genai SDK standards and confusing the agent during code generation.
  3. API Guide Overlaps & Fragmentation Across Repositories: The gemini-api skill in google/skills (which exists as an enterprise Agent Platform guide) collided directly with gemini-api-dev in google-gemini/gemini-skills. Installing across multiple Google repositories resulted in competing guides for the same underlying APIs (gemini-api, gemini-api-dev, gemini-agents-api, gemini-interactions-api, and gemini-live-api-dev).
  4. Irrelevant Domain Bundles (The Monolithic Repo Problem): Pulling down the monolithic google/skills repository imported mobile advertising suites (Android/iOS banner ads, Unity SDKs) that had zero relevance to cloud backend architecture and agent development.
  5. Hierarchical Sub-Skill Redundancies (The Meta-Skill Tax): 25 individual SDLC skills from addyosmani/agent-skills (such as test-driven-development and code-review-and-quality) were loaded into system prompt context at startup, despite being child sub-skills already orchestrated on-demand by using-agent-skills. Similarly, 20 sub-skills from google/agents-cli were loaded continuously rather than routed via google-agents-cli-workflow.
  6. Tooling & MCP Surface Overlaps: Standalone tools like the adk-docs-mcp server were redundant because google-developer-knowledge already indexes ADK documentation (adk.dev).
  7. Capability & Persona Redundancies: documentation-and-adrs (addyosmani/agent-skills) was a duplicate of architecture-decision-records (wshobson/agents), while generic content-creator (shubhamsaboo/awesome-llm-apps) collided with my custom dazbo-content persona.

What Else Did I Discover?

Beyond the sheer volume of redundant skills, I made another cool discovery: many skill collections come with a “parent” Meta-Skill.

When you install a large collection of skills — such as Addy Osmani’s engineering skills (addyosmani/agent-skills), or Google's 20 Agent Platform skills (google/agents-cli) — your instinct is to leave all 20 to 25 skills enabled in your workspace. I mean... Why wouldn't you?

However, these skill suites are designed to be hierarchical:

  • addyosmani/agent-skills: Orchestrated by the using-agent-skills meta-skill.
  • Google ADK & Agent Platform: Orchestrated by the google-agents-cli-workflow meta-skill.
  • GCP Data Pipelines: Orchestrated by the gcp-data-pipelines meta-skill.

In each case, the parent meta-skill is supposed to work as a sort of skills index or decision tree. When a specific task comes in — such as writing unit tests or running a security audit — the parent meta-skill directs the agent to fetch and read only the specific child sub-skill from disk on demand.

Parent meta-skill routing

Conclusion? We don’t need to load the frontmatter of all the child skills. We only need the parent skill! In the examples above, I can easily replace nearly 50 skills with just 3 orchestration skills!

So now I could implement a solution to organise my skills. The primary objectives:

  • Remove skills that are truly redundant.
  • Prevent Level 1 skills loading (i.e. reading frontmatter into context) for skills that have parent meta-skills. For these, only pre-load the parent meta-skills.

Clarifying Skill States

To design a lean agent workspace, we must be crystal clear on the three distinct states a skill can occupy throughout its lifecycle. They can be:

  • Installed and inactive (discoverable).
  • Installed and excluded.
  • Activated.

Let’s define these in more detail:

  1. Installed and inactive (discoverable): These skills are present on disk and discoverable by our agent. When the agent (e.g. Antigravity) starts, their frontmatter is automatically read into context.
  2. Installed and excluded: These skills reside in your disk library but are excluded from automatic Level 1 loading. For these, the agent does not automatically read their frontmatter and is therefore not directly aware of them during your conversation with it. But because they are present on disk and available to the agent, they can still be explicitly activated.
  3. Activated: This is the runtime state when a skill’s full SKILL.md body has been loaded into the current turn context, i.e. Level 2 loading (and Level 3, where supporting files are present and appropriate).

Crucially, both installed and inactive and installed and excluded skills can transition into this Activated state.

Skill states

Excluding Skills

So now we know that a good optimisation strategy is to use exclusion to prevent a bunch of skills being loaded at startup into the “Installed and inactive” state.

But how can we do this?

In Google Antigravity, global skill exclusions are managed via ~/.gemini/config/skills.json (or .agents/skills.json for workspace-level skills).

The native exclude array accepts skill folder names. For example:

{
  "exclude": [
    "alloydb-basics",
    "cloud-spanner-migrations",
    "firebase-basics"
  ]
}

Any skill listed in the exclude array is completely skipped during Tier 1 discovery. Its frontmatter is therefore not injected into the system prompt, saving tokens instantly.

So this is easy!

But here’s a cool trick. You can easily “un-exclude” a skill by prefixing its name with // inside the quotes. This is useful because it allows us to re-enable up-front skill loading, without having to check our disk to find the skill and its name.

So my approach is to always have all my available skills in the exclude list, but to comment-out the ones that need to be discovered automatically.

Note: I have the // inside the quotes. Why? Because this is not valid JSON:

{
  "exclude": [
    //"alloydb-basics",
    //"find-skills",
    "firebase-basics"
  ]
}

But this is:

{
  "exclude": [
    "//alloydb-basics",
    "//find-skills",
    "firebase-basics"
  ]
}

And because the exclude list uses exact string matching, you can use this // trick to enable/exclude skills on the fly, without having to remove the skill from the exclude list entirely.

In the example above:

  • alloydb-basics and find-skills are not excluded, so they are auto-discovered on startup and their frontmatter is loaded into context.
  • firebase-basics is excluded, so it is not auto-discovered on startup and its frontmatter is not loaded into context.

Now we’ve got everything we need to implement a repeatable mechanism to organise and clean up our skills.

Irony and Guitars: I Made a Skill!

Because: when you’ve got too many skills, you definitely need one more. (In this way, skills are a lot like guitars, or cats.)

Skills are like guitars and cats

Anyway, based on what I learned, I created a skill that:

  1. Identifies duplicative skills and recommends complete removal of those that are redundant.
  2. Identifies which skills are orchestrated by a parent meta-skill and excludes the child skills from automatic discovery.
  3. Identifies skills which look particularly niche or rarely used, and recommends excluding these also. They can always be included later, or even activated by explicitly asking the agent to do so.

Let’s see how it performs! After installing my skill, I said to Antigravity:

“Optimise my skills please”

Outcomes of Optimising

My skill ran and presented this proposal summary table:

Metric Baseline (Unoptimised) Proposed (Optimised) Delta / Savings
Installed Skills Directories 170 165 -5 (-2.9%)
Installed & Discoverable Skills 170 49 -121 (-71.2%)
Installed & Excluded Skills 0 116 +116
Prompt Token Overhead ~17,000 tokens ~3,350 tokens -13,650 tokens (-80.3%)

It goes on to say:

Upon your confirmation, I will execute the following steps:

1. **Retire Redundant Skills**: Delete the 5 redundant skill directories
   (`gemini-managed-agents-api`, `liveapi-service`, `documentation-and-adrs`, 
   `gemini-api-dev`, `gemini-live-api-dev`) from `~/.gemini/config/skills/`.
2. **Generate Optimised `skills.json`**: Write the full-inventory 
   `~/.gemini/config/skills.json` with 49 active skills (`//` prefix) and 
   116 excluded skills.
3. **Verify Context Footprint**: Re-audit the active skill prompt token 
   overhead to confirm successful reduction to ~3,350 tokens.

---
> [!IMPORTANT]
> **Mandatory Guardrail Confirmation**: In accordance with 
  `organise-agent-skills` guardrails, please confirm if you would like me 
  to proceed with deleting the 5 redundant skill directories and creating 
  the optimised `~/.gemini/config/skills.json`.

By trimming back to a core of 49 active skills and delegating the rest to meta-skills and on-demand rules:

  • We slashed system prompt overhead by approximately 13,650 tokens; that’s an 80.3% reduction.
  • We saved over 400,000 tokens per pair-programming session.
  • Agent response latencies dropped noticeably.
  • Tool selection precision increased because the agent was no longer fighting distractor noise from over 100 overlapping skill descriptions. Instead of having a mild existential crisis every turn trying to choose between five different Gemini SDK guides, the agent simply picks the right tool and gets on with the job.

Optimisation results

Summary & Conclusion

Agent skills are awesome. They’re so easy to find, install and use. But when we allow skills to sprawl unchecked, we start confusing our agents. This impacts accuracy, reliability, performance, and cost.

So increasingly, we need a way to manage and optimise our skills. For this, I’ve done some work so you don’t have to!

Feel free to download my optimisation skill off-the-shelf:

# Install all the Dazbo Agent Skills
npx skills add https://github.com/derailed-dash/dazbo-agent-skills

# Install just the `Organise Agent Skills` skill
npx skills add https://github.com/derailed-dash/dazbo-agent-skills --skill organise-agent-skills

Or if you have the Vercel find-skills skill installed, you can just ask your agent:

“Install dazbo-agent-skills for me.”

So, that’s it, folks. Go forth and optimise. If you find this skill useful, please give the repo a star.

Have you experienced Skills Sprawl in your agentic environment? How many skills are currently active in your setup? Let me know in the comments below!

Before You Go

  • Please share 📢 this with anyone that you think will be interested. It might help them, and it really helps me!
  • Please give me loads of reactions / hearts! 💖
  • Please leave a comment 💬. Interaction is good!
  • Add a star ⭐ on my repos!
  • Follow 👉 and subscribe 🔔, so you don’t miss my content.

References and Useful Links

Standards, Hubs & Agent Platforms

Related Dazbo Articles & Repositories

Key Agent Skills Repositories

Academic Research & Benchmarks

Read the whole story
alvinashcraft
10 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Building Enterprise Active Directory, LDAP & Dynamic RBAC in Go & Flutter with Google Antigravity

1 Share

Building Enterprise Active Directory, LDAP & Dynamic RBAC in Go & Flutter with Google Antigravity

When building a lightweight container orchestrator like Gubernator (gbnt) — designed to strike the perfect balance between the simplicity of Docker Swarm and the flexibility of Nomad under a Roman Empire theme — a critical milestone inevitably emerges: Enterprise Security and Access Control.

While a default admin credential works well for local dev environments, moving into enterprise production with multi-disciplinary engineering teams demands:

  1. Corporate Single Sign-On (SSO) with Microsoft Active Directory and OpenLDAP.
  2. Role-Based Access Control (RBAC) to clearly segregate who can deploy stacks, restart containers, or audit telemetries in read-only mode.
  3. Dynamic Group Mapping from corporate security groups (memberOf) to orchestrator roles.
  4. Emergency Break-Glass Access (Local Administrator) in case network directory controllers are unreachable.

In this article, we explore the complete architecture of the enterprise security engine introduced in Gubernator v2.20.0, and how we leveraged Google Antigravity (AGY) as an autonomous AI pair programmer to design, implement, test, and verify this Full-Stack feature (Go + Flutter Web) across a live 3-node cluster.

The Security Architecture

We designed a decoupled, asymmetric architecture connecting identity providers, REST API middleware, and the Flutter Web UI:

 ┌────────────────────────────────────────────────────────┐
 │                   GUBERNATOR WEB UI                    │
 │   - Modern Login Screen with Domain / AD Selector      │
 │   - Header Role Badge: Admin |  Ops |  Read-Only.      │
 └──────────────────────────┬─────────────────────────────┘
                            │ (REST /api/auth/login)
                            ▼
 ┌────────────────────────────────────────────────────────┐
 │             GUBERNATOR CORE AUTH ENGINE (Go)           │
 │  - Local Emergency Admin (admin / admin fallback)      │
 │  - Multi-Server Active Directory / OpenLDAP Dialers    │
 │  - LDAPS (Port 636) & StartTLS (Port 389) Handshake    │
 │  - Dynamic Group DN -> RBAC Role Resolution            │
 │  - Cryptographic HMAC-SHA256 JWT Token Signing         │
 └─────────────┬────────────────────────────┬─────────────┘
               │                            │
               ▼                            ▼
 ┌───────────────────────────┐ ┌──────────────────────────┐
 │  Primary Active Directory │ │ Secondary LDAP Server    │
 │   dc1.corporate.local     │ │   dc2.dr-site.local      │
 └───────────────────────────┘ └──────────────────────────┘

Role-Based Access Control (RBAC) Matrix

We established three distinct operational tiers:

Operational Capability admin operator readonly
Overview, Metrics & SRE Telemetry ✅ Full ✅ Full ✅ Full
Deploy Stacks (docker-compose.yml) ✅ Full ✅ Full ❌ Restricted
Redeploy & Duplicate Stacks ✅ Full ✅ Full ❌ Restricted
Delete Stacks ✅ Full ❌ Restricted ❌ Restricted
Task Lifecycle (Start / Stop / Restart) ✅ Full ✅ Full ❌ Restricted
Container & Node Terminal Shell ✅ Full ✅ Full ❌ Restricted
Node Fleet Management (Drain / Activate / Leave) ✅ Full ❌ Restricted ❌ Restricted
Caddy TLS Certificates & Ingress Routes ✅ Full ❌ Restricted ❌ Restricted
Active Directory & LDAP Directory Settings ✅ Full ❌ Restricted ❌ Restricted
Grafana, Jaeger & Weave Scope Dashboards ✅ Full ✅ Full ✅ Full

💻 The Go Backend Engine (internal/auth/)

For LDAP/Active Directory interactions, we used github.com/go-ldap/ldap/v3, and for session management github.com/golang-jwt/jwt/v5.

1. Two-Phase Bind & Credential Verification

Authentication follows a secure two-phase pattern:

  1. Connect and perform a Service Account Bind (BindDN / BindPassword) to query the directory.
  2. Search for the user object using a configurable LDAP filter (defaulting to (&(objectClass=user)(sAMAccountName=%s))).
  3. Open a secondary connection and perform a Direct User Bind with the user-submitted password against the domain controller.
func AuthenticateLDAP(cfg db.LDAPConfig, username, password string) (*AuthResult, error) {
    conn, err := ConnectLDAP(cfg)
    if err != nil {
        return nil, err
    }
    defer conn.Close()

    // 1. Initial service account bind
    if cfg.BindDN != "" && cfg.BindPassword != "" {
        if err := conn.Bind(cfg.BindDN, cfg.BindPassword); err != nil {
            return nil, fmt.Errorf("service account bind failed: %w", err)
        }
    }

    // 2. Search for the user
    filter := fmt.Sprintf(cfg.UserFilter, ldap.EscapeFilter(username))
    searchReq := ldap.NewSearchRequest(
        cfg.BaseDN,
        ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
        filter,
        []string{"dn", "displayName", "mail", "memberOf"},
        nil,
    )
    sr, err := conn.Search(searchReq)
    if err != nil || len(sr.Entries) == 0 {
        return nil, errors.New("user not found in directory")
    }

    userEntry := sr.Entries[0]

    // 3. Direct user bind to verify password
    userConn, err := ConnectLDAP(cfg)
    if err != nil {
        return nil, err
    }
    defer userConn.Close()

    if err := userConn.Bind(userEntry.DN, password); err != nil {
        return nil, errors.New("invalid credentials")
    }

    // 4. Map groups to RBAC role
    groups := userEntry.GetAttributeValues("memberOf")
    role := ResolveRole(cfg, groups)

    return &AuthResult{
        UserDN:      userEntry.DN,
        Username:    username,
        DisplayName: userEntry.GetAttributeValue("displayName"),
        Email:       userEntry.GetAttributeValue("mail"),
        Groups:      groups,
        Role:        role,
    }, nil
}

2. Dynamic Group-to-Role Mapping

Gubernator inspects the user's memberOf group list and matches them against the configured group DNs:

func ResolveRole(cfg db.LDAPConfig, userGroups []string) Role {
    matchesGroup := func(targetGroup string) bool {
        if targetGroup == "" { return false }
        target := strings.ToLower(strings.TrimSpace(targetGroup))
        for _, g := range userGroups {
            if strings.ToLower(strings.TrimSpace(g)) == target {
                return true
            }
        }
        return false
    }

    if matchesGroup(cfg.AdminGroupDN) { return RoleAdmin }
    if matchesGroup(cfg.OperatorGroupDN) { return RoleOperator }
    if matchesGroup(cfg.ReadOnlyGroupDN) { return RoleReadOnly }

    return NormalizeRole(cfg.DefaultRole)
}

The Flutter Web UI Experience

Gubernator's Web Dashboard is built with Flutter Web and Material Design 3, compiled and embedded directly into the Go binary (go:embed).

1. Modern Login Screen with Domain Selector

Operators can select their target authentication provider (Corporate Active Directory, DR Site LDAP, or Local Administrator):

Login Screen

2. Active Directory Management & Diagnostics

In the new Seguridad & AD tab, cluster administrators can configure directory servers, TLS certificates, and run a live "Test Connection" diagnostic tool:

Security & AD Management

3. Real-Time Role Badges & Contextual Guards

The dashboard header displays the active user and their assigned role (ADMIN, ⚡ OPERATOR, READ-ONLY). Mutating actions (e.g., Delete Stack, Drain Node, Shell) are automatically disabled for read-only audit accounts.

How Google Antigravity Accelerated Development

We utilized Google Antigravity (AGY) as an autonomous AI pair programmer to build this feature end-to-end. AGY accelerated the development cycle through several key workflows:

  1. Architectural Planning:
    Before writing code, Antigravity produced a comprehensive implementation plan (implementation_plan.md) outlining the GORM schema changes (LDAPConfig), RBAC authorization matrix, and API routes.

  2. Synchronized Full-Stack Implementation:
    In a single coordinated session, Antigravity:

    • Built the Go internal/auth/ engine with LDAP dialers, JWT session handlers, and Gin middlewares.
    • Applied SQLite database auto-migrations.
    • Implemented the Flutter Web UI (login_screen.dart, security_page.dart, and state models).
    • Updated existing views (legions_page.dart, tasks_page.dart, centurions_page.dart) with RBAC permission guards.
  3. Live Cluster Testing & Verification:
    Using automated commands across a 3-node multipass cluster (gbnt-manager, gbnt-worker1, gbnt-worker2), Antigravity:

    • Deployed and hot-restarted the ARM64 binaries.
    • Tested REST endpoints via curl (valid login, invalid login, LDAP connection tests, configuration lifecycle).
    • Executed Go unit tests (go test ./internal/auth/...) with 100% pass rates.
  4. Automated Documentation & Release:

    • Generated high-fidelity visual UI showcases.
    • Authored complete documentation in docs/auth-rbac.md and validated MkDocs builds in strict mode.
    • Bumped the version to v2.20.0, created git release tags, and triggered GitHub Pages publishing.

Conclusion & Open Source

Adding Active Directory SSO and RBAC allows teams to deploy Gubernator in enterprise production environments that require enterprise security compliance without the operational overhead of Kubernetes.

Check out Gubernator and try it out:

What do you think about this hybrid approach to container orchestration? Let us know your thoughts and suggestions in the comments!

Read the whole story
alvinashcraft
10 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Use Aspire to implement and deploy the BFF security architecture

1 Share

This blog demonstrates how to use Aspire to set up a solution for developing and deploying an ASP.NET Core web application with Auth0 as the identity provider and a downstream API. The application uses Angular for the frontend and is secured using a Backend-for-Frontend (BFF) architecture.

Code: https://github.com/damienbod/Auth0BffDpopApi

Blogs in this series

  1. Implement BFF using Auth0, Angular and ASP.NET Core
  2. Use Aspire to implement and deploy the BFF security architecture
  3. Implement secure downstream APIs using DPoP and Auth0

Target setup

In this setup, it is planned to implement the recommended authentication for applications and users which uses best practices and recommended authentication flows.

Aspire Setup

Aspire maps all the applications together using a code configuration setup in the AppHost class. This class allows for a development and production setup. The configuration, the connects and other deployment settings can be defined in this class.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;

var builder = DistributedApplication.CreateBuilder(args);

// Used in the applications when called downstream APIs, etc
const string WEB_APPLICATION = "web-app-bff-service";
const string API_SERVICE = "api-service"; 

IResourceBuilder<ProjectResource>? webApi = null;
IResourceBuilder<ProjectResource>? webApplication = null;

// Parameters for the web application
var webOidcClientPrivatePem = builder.AddParameter("WebOidcClientPrivatePem", secret: true);
var webOidcClientPublicPem = builder.AddParameter("WebOidcClientPublicPem");
var webDpopClientPrivatePem = builder.AddParameter("WebDpopClientPrivatePem", secret: true);
var webDpopClientPublicPem = builder.AddParameter("WebDpopClientPublicPem");

var webAuth0Authority = builder.AddParameter("WebAuth0Authority");
var webAuth0Audience = builder.AddParameter("WebAuth0Audience");
var webAuth0Domain = builder.AddParameter("WebAuth0Domain");
var webAuth0ClientId = builder.AddParameter("WebAuth0ClientId");
var webAuth0CallbackPath = builder.AddParameter("WebAuth0CallbackPath");

// Parameters for the web API
var apiAuth0Authority = builder.AddParameter("ApiAuth0Authority");
var apiAuth0Audience = builder.AddParameter("ApiAuth0Audience");
var apiAuth0Domain = builder.AddParameter("ApiAuth0Domain");
var apiDeploySwaggerUI = builder.AddParameter("ApiDeploySwaggerUI");

webApi = builder.AddProject<Projects.WebApi>(API_SERVICE)
    .WithExternalHttpEndpoints()
    .WithEnvironment("Auth0:Authority", apiAuth0Authority)
    .WithEnvironment("Auth0:Audience", apiAuth0Audience)
    .WithEnvironment("Auth0:Domain", apiAuth0Domain)
    .WithEnvironment("DeploySwaggerUI", apiDeploySwaggerUI);

if (builder.Environment.IsDevelopment())
{
    var angularFrontend = builder.AddJavaScriptApp("angular", "../bff/ui", "start")
        .WithHttpsEndpoint(port: 3000, 4201, env: "BASE_URL");

    webApplication =builder.AddProject<Projects.BffAuth0_Server>(WEB_APPLICATION)
        .WithExternalHttpEndpoints()
        .WithReference(angularFrontend)
        .WaitFor(angularFrontend)
        .WithReference(webApi)
        .WaitFor(webApi)
        .WithEnvironment("Auth0:Authority", webAuth0Authority)
        .WithEnvironment("Auth0:Audience", webAuth0Audience)
        .WithEnvironment("Auth0:Domain", webAuth0Domain)
        .WithEnvironment("Auth0:ClientId", webAuth0ClientId)
        .WithEnvironment("Auth0:CallbackPath", webAuth0CallbackPath)
        .WithEnvironment("OidcClientPrivatePem", webOidcClientPrivatePem)
        .WithEnvironment("OidcClientPublicPem", webOidcClientPublicPem)
        .WithEnvironment("DpopClientPrivatePem", webDpopClientPrivatePem)
        .WithEnvironment("DpopClientPublicPem", webDpopClientPublicPem);
}
else
{
    // Hint: to make this work, the deployment pipeline must execute npm run build 
    // which deploys to the wwwroot folder of the bffauth0-server project.
    webApplication = builder.AddProject<Projects.BffAuth0_Server>(WEB_APPLICATION)
        .WithExternalHttpEndpoints()
        .WithReference(webApi)
        .WaitFor(webApi)
        .WithEnvironment("WebAuth0Authority", webAuth0Authority)
        .WithEnvironment("WebAuth0Audience", webAuth0Audience)
        .WithEnvironment("WebAuth0Domain", webAuth0Domain)
        .WithEnvironment("WebAuth0ClientId", webAuth0ClientId)
        .WithEnvironment("WebAuth0CallbackPath", webAuth0CallbackPath)
        .WithEnvironment("WebOidcClientPrivatePem", webOidcClientPrivatePem)
        .WithEnvironment("WebOidcClientPublicPem", webOidcClientPublicPem)
        .WithEnvironment("WebDpopClientPrivatePem", webDpopClientPrivatePem)
        .WithEnvironment("WebDpopClientPublicPem", webDpopClientPublicPem);
}

builder.Build().Run();

Adding Aspire to the projects/applications

Aspire provides a default AppsAspire.ServiceDefaults project which is referenced from each ASP.NET Core project. The Aspire AppHost project can then reference the different apps and is configured in the host project.

Aspire configuration

All configuration properties need to be setup in the AppHost Aspire project which links all the containers and apps together. The routes and paths are automatically set correctly, when the different projects are referenced using the Aspire helper methods.

When using different APIs, the path can be matched using the name of the service from the AppHost file. Then the path gets mapped correctly using Aspire for all deployments.

builder.Services.AddUserAccessTokenHttpClient("dpop-api-client", configureClient: client =>
{
    // See App Host for the api-service definition. This is the name of the service in the AppAspireHost project.
    client.BaseAddress = new("https+http://api-service");
});

YARP is used in both development and production. The YARP configuration is read through the code configuration can the values are setup using the AppHost from Aspire. The app.settings are used for local development, not for production. This is not required, just how I set this up.

if (builder.Environment.IsDevelopment())
{
    // Development
    builder.Services.AddReverseProxy()
   .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
}
else
{
    // Production
    // Support for Aspire and Containers
    builder.Services.AddReverseProxy()
    .LoadFromMemory(YarpConfigurations.GetProductionRoutes(),
        YarpConfigurations.GetProductionClusters(builder.Configuration["DownstreamApiUrl"]!));
}

The WithEnvironment adds the parameters to the different containers as configuration. These values can be used like in any ASP.NET Core application.

Dev setup

The solution uses a backend for frontend architecture. The AddJavaScriptApp method adds a host project for the UI app which maps to the default dev route. This is only used in development, so that aa UI dev can use his or her preferred tools.

Notes

The Auth0 client NuGet client requires app.settings which cannot be changed and these values must be passed in as defined by the Auth0 client NuGet package. The user info endpoint does not work when using a client assertion setup with DPoP.

Links

https://auth0.com/docs/quickstart/webapp/aspnet-core

https://auth0.com/blog/backend-for-frontend-pattern-with-auth0-and-dotnet

https://github.com/damienbod/bff-auth0-aspnetcore-angular

https://github.com/damienbod/DPOP-aspnetcore-idp

https://auth0.com/docs/secure/sender-constraining/demonstrating-proof-of-possession-dpop

https://auth0.com/blog/implementing-dpop-with-auth0

https://auth0.com/docs/quickstart/backend/aspnet-core-webapi#using-dpop-for-enhanced-security



Read the whole story
alvinashcraft
10 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Your Internal Docs Need the Same Scoring Discipline as Your Public APIs

1 Share

Joyce Stack has published a set of Claude Code skills that do information-architecture analysis on a Markdown documentation corpus, and it is the most directly useful enterprise work I have seen come out of the skills format so far. The plugin is markdown-ia-skills. It is three skills meant to run in sequence against a git repo or a folder of .md and .mdx files. The first one does reconnaissance and answers what exists, how it is organized, and how mature it looks. The second infers the content model that the corpus already has, without you supplying a schema, and proposes document types with required and optional metadata. The third analyzes terminology and taxonomy, and produces a canonical termbase alongside a facet design. Each one runs on find, grep, and reading files. There is no bundled script to install and nothing to send anywhere.

I care about this because the enterprise Markdown corpus is now an agent surface and almost nobody treats it as one. Every company I talk to has a docs repo, a pile of ADRs, a runbook folder, a Confluence export somebody dumped into Git, and lately a growing stack of CLAUDE.md files and internal skills. That content was written for humans who already knew where things were. Now agents read it, and an agent has none of the tribal context that made an unlabeled folder navigable. It cannot tell that guides/ actually holds three policy documents. It cannot tell that “ArgoCD” and “Argo CD” are the same product. It just answers, confidently, using whichever page it landed on.

The sample report in the repo makes that concrete. Joyce ran all three skills against the Argo CD documentation, which is 456 Markdown files with over a thousand contributors, and found that zero of them use tags, categories, labels, domain, or topic. Every bit of classification in that corpus is implicit, carried by folder names and an mkdocs.yml nav file. That is not a criticism of Argo CD, and it would be the finding at most enterprises too. It is simply what a documentation corpus looks like when it grows by accretion, and it is a very different thing to hand an agent than a corpus with a content model.

What made me want to write this up is how much of the discipline overlaps with the Kin Score work, pointed inward instead of outward. I score what a company publishes to the world. Joyce is reading what a team wrote for itself. The mechanics of doing either one honestly turn out to be the same in four specific ways.

The first is calibrating findings by type. Her content-model skill sorts every document type into a governance artifact or reference content before it flags anything. A standard, a policy, or an ADR needs its own owner and status and review date, because its currency has to be tracked independently. A CLI reference does not, because git history already tells you whether it is current. Missing metadata on the first is a real gap. On the second it is marked N/A, at zero percent coverage, without apology. That is the same reasoning behind the conditional regulatory facet in the Kin Score, where a set of checks applies to a bank and not to a weather API. A rubric that applies every check to everything produces a number nobody trusts.

The second is refusing false positives. In the vocabulary report, a regex candidate called “Side Apply” got ruled out, because it was matching inside both “client-side apply” and “server-side apply,” two distinct Kubernetes concepts being used correctly. She wrote the ruling into the report rather than quietly dropping the row. I have spent a lot of this year building exactly that muscle in the catalog, where a populated directory is not evidence a provider published anything, and a soft 200 is not evidence an API exists. A match is not a finding. Saying out loud what you ruled out is what separates an assessment from a word count.

The third is that every claim cites its evidence. The reports name file paths and occurrence counts, so 258 uses of “ArgoCD” against 3,744 uses of “Argo CD” is a number you can go rerun yourself. That is the same contract as an evidence table where every row is a URL and a status code.

The fourth is register. Both discovery skills cap themselves at roughly 400 to 600 words of prose and instruct the model to write “looks like a de facto required field” rather than “this is mandatory.” That is a first pass, deliberately, and it says so. I have made the same call about the catalog, which is built for discovery rather than compilation, and refining week to week beats stalling on total precision. An informed fast read that someone acts on this afternoon is worth more than an audit that arrives next quarter.

Where the two diverge is comparison. The Kin Score exists to place a provider against its peers, in a band, inside an area, so a vendor can see what it would take to move up and a buyer can shop inside a category. Joyce’s skills are single-corpus and diagnostic. There is no band and no cross-company cut, and for internal documentation that is the right call, because there is no market to place yourself in and the audience is the team that owns the content. It does raise the obvious next question of what an internal maturity band would even mean across a hundred repos in one enterprise, which is a problem I would happily spend time on.

The last thing worth noting is the packaging. Three skills that compose but each stand alone, general-purpose tools instead of a dependency, an evals file, and sample output from a real corpus you can go verify. I have written before that skills bring real value and a fresh set of problems in the same package. This is a good answer to the value half. If you are pointing an agent at your internal documentation, run something like this against the corpus first, because right now you are asking it to navigate a structure that only exists in your head.



Read the whole story
alvinashcraft
10 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories