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

I'm swearing off APIs entirely

1 Share

I got a lot of ideas for side projects rattling around in the old tin can. As part of my “No new projects” initiative, I’m trying to jump on building prototypes so I can decide if I want to explore ideas more or call it quits. A handful of my ideas are riffs or twists on existing app categories:

  • A tennis ranking app… but modern and performant
  • A nearby historical marker app… but with CarPlay support
  • A nearby real estate listing app… but with CarPlay support

All three of those have ended unceremoniously at the same dead end: no API access. USTA denied my application for tennis rankings. The aptly named Historical Marker Database site doesn’t have a public API. And you have to be an MLS® Realtor® or Broker to get access to the MLS® listings. Womp womp.

Scraping the data is always an option… but I don’t like the ethics of that and worry about the brittleness of that dependency.

Ugh. I wish I could build these little apps so that tens of people could enjoy them. I’d even be willing to pay a small API access fee ($10/mo?) and run these at a loss but whatever happened to free APIs. When I survey the land of public APIs it feels like we’ve lost a lot since the Web 2.0 days where API access was almost a God-given right.

To prevent this time loop of disappointment from happening again, I’m swearing off APIs entirely. That’s a hard stance, but I need a backstop at the idea phase to prevent me from wasting limited life force. If I don’t have the data, or can’t generate the data, or it’s not an open protocol… it’s not worth building or even thinking about.

OAuth apps are a good option and generally the best way to exfiltrate data because it’s tied to a user’s account…. but you still might run into call limits, incomplete endpoints, user-scope limitations, and so on. History also shows us what the future holds. There’s a Tweetbot-style risk when building on a someone else’s platform. Even if your app drives activity to the parent application, your access might get cut because it competes or doesn’t drive stakeholder value. And if the idea isn’t big enough, being “a feature, not a product” is also a bad position to be in, lest you get Sherlock’d.

Where’s that leave me and my pile of side project ideas? Thankfully… in a good place. I can close out these project tabs and free up some much needed Brain RAM. It sounds strange but “No more APIs” makes “Making video games” jump up in the viability rankings for side projects too, because games have closed ecosystems. Or I could spend more time writing shitty sci-fi. Write a serial. Print some zines. Who knows.

If the goal of “No new projects” is to finish more projects than I start, then I have to accept that part of figuring out which ideas to explore means “Nope” is a potential answer. It’s also not a total loss, I’m learning along the way. For example, CarPlay only lets you choose from eight pre-approved templates. There’s also pre-defined app categories and diverting in the slightest would almost guarantee App Store rejection. That sucks the fun out it… but ayyyy, I’ll probably try again. But now I know the limitation for future projects and its in the limitations where play begins.

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

Force step up authentication in web applications

1 Share

The post shows how to implement a step up authorization using the OAuth 2.0 Step Up Authentication Challenge Protocol RFC 9470. The application uses ASP.NET Core to implement the API, the web application and the identity provider. Duende IdentityServer is used to implement the OpenID Connect server standard and also OAuth DPoP token binding as well as other OAuth standards.

Code: https://github.com/swiss-ssi-group/swiyu-passkeys-idp-loi-loa

Blogs in this series:

Setup

The solutions uses multiple containers in a container hosting environment. I use Azure Container Apps as my preferred solution for cloud hosting deployments.

The API requires access tokens and forces OAuth DPoP token binding. The API uses Open API to describe the endpoint. If the DPoP access token is missing or has an incorrect value or not the required claims, a 401 is returned with the WWW-Authenticate set using the OAuth specification.

The web application uses OpenID Connect to authenticate as well as requiring DPoP access tokens. The access token is used to request data from the downstream API. If a 401 is returned, the web application provides a way to authenticate again using the required authentication and identification.

The identity provider muss handle the step up authentication request. This is implemented by Duende IdentityServer using the AuthorizeInteractionResponseGenerator base class. This handles all login requests, not just the step requests. Multiple login flows needs to be supported and tested when implementing this.

The identity provider container uses ASP.NET Core Identity with an SQL Server database. The database is migrated using a .NET Worker service using Entity Framework Core migrations. The database uses passkeys and swiyu tables to store the identity data.

Swiyu is supported using the generic containers which implement the swiyu Public Beta infrastructure. The swiyu verifier container supports both management APIs and OpenIDVP implementations. The Swiss Wallet uses the public API to complete an identification check.

The applications are run and setup locally using Microsoft Aspire. This reduces the complexity of creating and hosting local containers and also makes is easy to deploy the professional environments like Azure Container Apps. You could also use AKS, but this makes no sense implementing a low level container hosting system.

Implement the API

An AuthorizationHandler is used to validate the level of authentication and the level of identification authorization requirements. The handler validates if the required claims has the required value.

using Idp.Swiyu.Passkeys.ApiService;
using Microsoft.AspNetCore.Authorization;

public class LoaHandler : AuthorizationHandler<LoaRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LoaRequirement requirement)
    {
        // DPoP is required to use the API
        var loa = context.User.FindFirst(c => c.Type == Consts.LOA);

        if (loa is null)
        {
            return Task.CompletedTask;
        }

        // Lets require passkeys to use this API
        if (loa.Value != Consts.LOA_400)
        {
            return Task.CompletedTask;
        }

        context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

The implementation of the IAuthorizationMiddlewareResultHandler is used to fulfil the OAuth 2.0 Step Up Authentication Challenge Protocol RFC 9470 specification. If the loi or the loa requirement fails, the WWW-Authenticate header is set with the correct value.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using System.Text;

namespace Idp.Swiyu.Passkeys.ApiService;

/// <summary>
/// https://datatracker.ietf.org/doc/rfc9470/ 
/// implementation for step-up authorization requirements
/// </summary>
public class ForbiddenAuthorizationMiddleware : IAuthorizationMiddlewareResultHandler
{
    private readonly AuthorizationMiddlewareResultHandler defaultHandler = new();

    public async Task HandleAsync(
        RequestDelegate next,
        HttpContext context,
        AuthorizationPolicy policy,
        PolicyAuthorizationResult authResult)
    {
        // If the authorization was forbidden due to a step-up requirement, set
        // the status code and WWW-Authenticate header to indicate that step-up
        // is required
        if (authResult.Forbidden)
        {
            var loaFailed = authResult.AuthorizationFailure!.FailedRequirements
                .OfType<LoaRequirement>().FirstOrDefault();
            var loiFailed = authResult.AuthorizationFailure!.FailedRequirements
                .OfType<LoiRequirement>().FirstOrDefault();

            if (loaFailed != null || loiFailed != null)
            {
                var errorMessage = new CreateErrorMessage();
                if (loaFailed != null)
                {
                    errorMessage.Loa = Consts.LOA_400;
                }
                if (loiFailed != null)
                {
                    errorMessage.Loi = Consts.LOI_400;
                }

                context.Response.Headers.WWWAuthenticate = errorMessage.GetErrorMessage();
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;

                return;
            }
        }

        // Fall back to the default implementation.
        await defaultHandler.HandleAsync(next, context, policy, authResult);
    }
}

public class CreateErrorMessage
{
    private readonly string Error = "insufficient_user_authentication";
    private string ErrorDescription
    {
        get
        {
            var errorDescription = new StringBuilder();

            if (Loi != null && Loa != null)
            {
                errorDescription.Append("insufficient level of identification and authentication");
            }

            if (Loi != null && Loa == null)
            {
                errorDescription.Append("insufficient level of identification");
            }

            if (Loa != null && Loi == null)
            {
                errorDescription.Append("insufficient level of authentication");
            }


            return errorDescription.ToString();
        }
    }

    public string? Loi { get; set; }
    public string? Loa { get; set; }

    public string GetErrorMessage()
    {
        var props = new StringBuilder();
        props.Append($"Bearer error=\"{Error}\",");
        props.Append($"error_description=\"{ErrorDescription}\", ");

        if (Loi != null && Loa != null)
        {
            props.Append($"{Consts.LOI}=\"{Loi}\", ");
            props.Append($"{Consts.LOA}=\"{Loa}\"");
        }

        if (Loi != null && Loa == null)
        {
            props.Append($"{Consts.LOI}=\"{Loi}\"");
        }

        if (Loa != null && Loi == null)
        {
            props.Append($"{Consts.LOA}=\"{Loa}\"");
        }

        return props.ToString();
    }
}

The API is setup to use DPoP access tokens to protected the data. If the DPoP access token is validated successfully, the authorization rules and the policies are validated. If the authorization fails, the WWW-Authenticate is set correctly and returned to the calling application. The audience and the issuer are validated as well like recommended in the different specifications used in implementation.

builder.Services.AddOpenApi();

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer(options =>
    {
        options.Authority = "https://localhost:5001";
        options.Audience = "dpop-api";

        options.TokenValidationParameters.ValidateAudience = true;
        options.TokenValidationParameters.ValidateIssuer = true;
        options.TokenValidationParameters.ValidAudience = "dpop-api";

        options.MapInboundClaims = false;
        options.TokenValidationParameters.ValidTypes = ["at+jwt"];
    });

// layers DPoP onto the "token" scheme above
builder.Services.ConfigureDPoPTokensForScheme("Bearer", opt =>
{
    opt.ValidationMode = ExpirationValidationMode.IssuedAt; // IssuedAt is the default.
});

builder.Services.AddAuthorization();

builder.Services.AddSingleton<IAuthorizationHandler, LoiHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, LoaHandler>();
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, ForbiddenAuthorizationMiddleware>();

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("authz_checks", policy => policy
        .RequireAuthenticatedUser()
        .AddRequirements([new LoaRequirement(), new LoiRequirement()]));

Implement the web application step up handling

Once the 401 is returned with the WWW-Authenticate set correctly, the web application needs to handle this correctly.

using System.Net;

namespace Idp.Swiyu.Passkeys.Web.WeatherServices;

public class WeatherApiClient
{
    private readonly IHttpClientFactory _httpClientFactory;
    public WeatherApiClient(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<WeatherForecast[]> GetWeatherAsync(int maxItems = 10, CancellationToken cancellationToken = default)
    {
        var httpClient = _httpClientFactory.CreateClient("dpop-api-client");

        HttpResponseMessage? response = null;
        try
        {
            // Make a direct request to check for 401 first
            response = await httpClient.GetAsync("/weatherforecast", cancellationToken);

            // Check if we got a 401 response
            if (response.StatusCode == HttpStatusCode.Unauthorized)
            {
                // Parse the WWW-Authenticate header to extract error_description
                var errorMessage = ApiErrorHandling.ParseErrorDescriptionFromResponse(response);
                throw new ApiErrorHandlingException(errorMessage);
            }

            // Ensure success status code
            response.EnsureSuccessStatusCode();

            // Read the response as an array
            var forecasts = await response.Content.ReadFromJsonAsync<WeatherForecast[]>(cancellationToken);

            // Take only maxItems
            if (forecasts != null && forecasts.Length > maxItems)
            {
                return forecasts.Take(maxItems).ToArray();
            }

            return forecasts ?? [];
        }
        finally
        {
            response?.Dispose();
        }
    }
}

public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

The ApiErrorHandling parses the error description depending on the error and returns this in the WWW-Authenticate header.

public static class ApiErrorHandling
{
    public static string ParseErrorDescriptionFromResponse(HttpResponseMessage response)
    {
        var errorMessage = new StringBuilder();
        errorMessage.Append($"Reason: {response.ReasonPhrase}, ");

        // Get the WWW-Authenticate header
        if (response.Headers.WwwAuthenticate.Any())
        {
            foreach (var authHeader in response.Headers.WwwAuthenticate)
            {
                var headerValue = authHeader.ToString();

                errorMessage.Append(headerValue);
            }
        }
        else
        {
            errorMessage.Append("Unauthorized access to API, WWW-Authenticate header not set");
        }


        return errorMessage.ToString();
    }
}

The web application displays the error in the UI and allows the user of the application to step up authentication.

@if (errorMessage != null)
{
    var returnUrl = NavigationManager.Uri;
    <div class="alert alert-danger" role="alert">
        <strong>Error:</strong> @errorMessage
        @if (errorMessage.Contains("loi", StringComparison.OrdinalIgnoreCase))
        {
            <div class="mt-2">
                <a class="btn btn-primary" href="@GetRegisterSwiyuUrl()" target="_blank">
                    <span class="bi bi-key-fill-nav-menu" aria-hidden="true"></span> Step up identification
                </a>
            </div>
        }
        @if (errorMessage.Contains("loa", StringComparison.OrdinalIgnoreCase))
        {
            var loaValue = ExtractParameterValue(errorMessage, "loa");
            if (!string.IsNullOrEmpty(loaValue))
            {
                var stepUpUrl = $"/stepuploa?loa={Uri.EscapeDataString(loaValue)}&returnUrl={Uri.EscapeDataString(returnUrl)}";
                <div class="mt-2">
                    <a href="@stepUpUrl" class="btn btn-primary">Step up authentication</a>
                </div>
            }
        }
    </div>
}

When a user selects the step up type and starts the flow, the backend application begins the OpenID Connect challenge. If the user needs to authenticate, the challenge sends the required acr_values prompt, if the user needs an identity verification, the user is redirected to start the OpenIDVP flow.

        app.MapGet("/stepuploa", async context =>
        {
            var returnUrl = context.Request.Query["returnUrl"];
            var loa = context.Request.Query["loa"];

            if (!string.IsNullOrEmpty(loa) && loa == "loa.400")
            {
                await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties
                {
                    RedirectUri = returnUrl == StringValues.Empty ? "/" : returnUrl.ToString(),
                    Items = { ["acr_values"] = "phr" }
                });
            }
            else
            {
                await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties
                {
                    RedirectUri = returnUrl == StringValues.Empty ? "/" : returnUrl.ToString(),
                    Items = { ["acr_values"] = "mfa" }
                });
            }

        }).AllowAnonymous();

Implement the OpenID Connect Server

The StepUpInteractionResponseGenerator implements the AuthorizeInteractionResponseGenerator class. This method is called every time a user tries to logout. If special logic is required to step up, the user gets redirected to the required UI.

namespace Idp.Swiyu.Passkeys.Sts;

public class StepUpInteractionResponseGenerator : AuthorizeInteractionResponseGenerator
{
    public StepUpInteractionResponseGenerator(
        IdentityServerOptions options,
        IClock clock,
        ILogger<AuthorizeInteractionResponseGenerator> logger,
        IConsentService consent,
        IProfileService profile) : base(options, clock, logger, consent, profile)
    {
    }

    protected override async Task<InteractionResponse> ProcessLoginAsync(ValidatedAuthorizeRequest request)
    {
        var result = await base.ProcessLoginAsync(request);

        if (!result.IsLogin && !result.IsError)
        {
            if (PasskeysRequired(request) && !AuthenticatedWithPasskeys(request.Subject!))
            {
                if (UserDeclinedMfa(request.Subject!))
                {
                    result.Error = OidcConstants.AuthorizeErrors.UnmetAuthenticationRequirements;
                }
                else
                {
                    // passkeys can be completed here
                    result.RedirectUrl = "/Account/Login";
                }
            }
            else if (MfaRequired(request) && !AuthenticatedWithMfa(request.Subject!))
            {
                if (UserDeclinedMfa(request.Subject!))
                {
                    result.Error = OidcConstants.AuthorizeErrors.UnmetAuthenticationRequirements;
                }
                else
                {
                    // Swiyu authentication possible
                    result.RedirectUrl = "/Account/Login";

                    // if you support the default Identity setup with MFA,
                    //result.RedirectUrl = "/Account/LoginWith2fa";
                }
            }
        }

        return result;
    }

    private bool PasskeysRequired(ValidatedAuthorizeRequest request) =>
       PasskeysRequestedByClient(request);

    private bool PasskeysRequestedByClient(ValidatedAuthorizeRequest request)
    {
        return request.AuthenticationContextReferenceClasses!.Contains("phr");
    }

    private bool MfaRequired(ValidatedAuthorizeRequest request) =>
       MfaRequestedByClient(request);

    private bool MfaRequestedByClient(ValidatedAuthorizeRequest request)
    {
        return request.AuthenticationContextReferenceClasses!.Contains("mfa");
    }

    private bool AuthenticatedWithMfa(ClaimsPrincipal user) =>
        user.Claims.Any(c => c.Type == "amr" && (c.Value == Amr.Pop || c.Value == Amr.Mfa));

    private bool AuthenticatedWithPasskeys(ClaimsPrincipal user) =>
        user.Claims.Any(c => c.Type == "amr" && c.Value == Amr.Pop);

    private bool UserDeclinedMfa(ClaimsPrincipal user) =>
        user.Claims.Any(c => c.Type == "declined_mfa" && c.Value == "true");
}

The service needs to be added to the STS services.

builder.Services.AddTransient<IAuthorizeInteractionResponseGenerator,
   StepUpInteractionResponseGenerator>();

When run, if the user is missing both the authentication requirement and the identification requirement, the web application displays the following error when trying to access the API.

If the used has authenticated using passkeys, but not completed an identity check:

Notes

This works good and communicates the level of authentication and the level of the identification to all clients of the OpenID Connect server. The solution still needs some further security hardening and the applications parts which are not required should be removed.

Links

https://github.com/dotnet/aspnetcore/issues/64881

https://openid.net/specs/openid-connect-eap-acr-values-1_0-final.html

https://datatracker.ietf.org/doc/html/rfc8176

https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims

SSI

https://www.eid.admin.ch/en/public-beta-e

https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview

https://www.npmjs.com/package/ngrok

https://swiyu-admin-ch.github.io/specifications/interoperability-profile/

https://andrewlock.net/converting-a-docker-compose-file-to-aspire/

https://swiyu-admin-ch.github.io/cookbooks/onboarding-generic-verifier/

https://github.com/orgs/swiyu-admin-ch/projects/2/views/2

SSI Standards

https://identity.foundation/trustdidweb/

https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html

https://openid.net/specs/openid-4-verifiable-presentations-1_0.html

https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/

https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/

https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/

https://www.w3.org/TR/vc-data-model-2.0/



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

Investing in Aurelia's Next Decade

1 Share

Aurelia turned 11 this year. That is older than some JavaScript frameworks that have come and gone entirely. It is older than many of the developers now discovering it for the first time. And somehow, against all odds in an ecosystem that treats frameworks like seasonal fashion, we are still here.

We just shipped the release candidate for Aurelia 2 . The framework is stable, the documentation is comprehensive, and production applications are running on it today. This is not a project on life support. This is a project that has been quietly, consistently delivering for over a decade.

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

Building Agents with GitHub Copilot SDK: A Practical Guide to Automated Tech Update Tracking

1 Share

Introduction

In the rapidly evolving tech landscape, staying on top of key project updates is crucial. This article explores how to leverage GitHub's newly released Copilot SDK to build intelligent agent systems, featuring a practical case study on automating daily update tracking and analysis for Microsoft's Agent Framework.

GitHub Copilot SDK: Embedding AI Capabilities into Any Application

SDK Overview

On January 22, 2026, GitHub officially launched the GitHub Copilot SDK technical preview, marking a new era in AI agent development. The SDK provides these core capabilities:

  • Production-grade execution loop: The same battle-tested agentic engine powering GitHub Copilot CLI
  • Multi-language support: Node.js, Python, Go, and .NET
  • Multi-model routing: Flexible model selection for different tasks
  • MCP server integration: Native Model Context Protocol support
  • Real-time streaming: Support for streaming responses and live interactions
  • Tool orchestration: Automated tool invocation and command execution

Core Advantages

Building agentic workflows from scratch presents numerous challenges:

  • Context management across conversation turns
  • Orchestrating tools and commands
  • Routing between models
  • Handling permissions, safety boundaries, and failure modes

The Copilot SDK encapsulates all this complexity. As Mario Rodriguez, GitHub's Chief Product Officer, explains:

"The SDK takes the agentic power of Copilot CLI and makes it available in your favorite programming language... GitHub handles authentication, model management, MCP servers, custom agents, and chat sessions plus streaming. That means you are in control of what gets built on top of those building blocks."

Quick Start Examples

Here's a simple TypeScript example using the Copilot SDK:

import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); await client.start(); const session = await client.createSession({ model: "gpt-5", }); await session.send({ prompt: "Hello, world!" });

And in Python, it's equally straightforward:

from copilot import CopilotClient client = CopilotClient() await client.start() session = await client.create_session({ "model": "claude-sonnet-4.5", "streaming": True, "skill_directories": ["./.copilot_skills/pr-analyzer/SKILL.md"] }) await session.send_and_wait({ "prompt": "Analyze PRs from microsoft/agent-framework merged yesterday" })

Real-World Case Study: Automated Agent Framework Daily Updates

Project Background

agent-framework-update-everyday is an automated system built with GitHub Copilot SDK and CLI that tracks daily code changes in Microsoft's Agent Framework and generates high-quality technical blog posts.

 

System Architecture

The project leverages the following technology stack:

  1. GitHub Copilot CLI (@github/copilot): Command-line AI capabilities
  2. GitHub Copilot SDK (github-copilot-sdk): Programmatic AI interactions
  3. Copilot Skills: Custom PR analysis behaviors
  4. GitHub Actions: CI/CD automation pipeline

Core Workflow

The system runs fully automated via GitHub Actions, executing Monday through Friday at UTC 00:00 with these steps:

StepActionDescription
1Checkout repositoryClone the repo using actions/checkout@v4
2Setup Node.jsConfigure Node.js 22 environment for Copilot CLI
3Install Copilot CLIInstall via npm i -g github/copilot
4Setup PythonConfigure Python 3.11 environment
5Install Python dependenciesInstall github-copilot-sdk package
6Run PR AnalysisExecute pr_trigger_v2.py with Copilot authentication
7Commit and pushAuto-commit generated blog posts to repository

Technical Implementation Details

1. Copilot Skill Definition

The project uses a custom Copilot Skill (.copilot_skills/pr-analyzer/SKILL.md) to define:

  • PR analysis behavior patterns
  • Blog post structure requirements
  • Breaking changes priority strategy
  • Code snippet extraction rules

This skill-based approach enables the AI agent to focus on domain-specific tasks and produce higher-quality outputs.

2. Python SDK Integration

The core script pr_trigger_v2.py demonstrates Python SDK usage:

from copilot import CopilotClient # Initialize client client = CopilotClient() await client.start() # Create session with model and skill specification session = await client.create_session({ "model": "claude-sonnet-4.5", "streaming": True, "skill_directories": ["./.copilot_skills/pr-analyzer/SKILL.md"] }) # Send analysis request await session.send_and_wait({ "prompt": "Analyze PRs from microsoft/agent-framework merged yesterday" })

3. CI/CD Integration

The GitHub Actions workflow (.github/workflows/daily-pr-analysis.yml) ensures automated execution:

name: Daily PR Analysis on: schedule: - cron: '0 0 * * 1-5' # Monday-Friday at UTC 00:00 workflow_dispatch: # Support manual triggers jobs: analyze: runs-on: ubuntu-latest steps: - name: Setup and Run Analysis env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} run: | npm i -g github/copilot pip install github-copilot-sdk --break-system-packages python pr_trigger_v2.py

Output Results

The system automatically generates structured blog posts saved in the blog/ directory with naming convention:

blog/agent-framework-pr-summary-{YYYY-MM-DD}.md

Each post includes:

  1. Breaking Changes (highlighted first)
  2. Major Updates (with code examples)
  3. Minor Updates and Bug Fixes
  4. Summary and impact assessment

Latest Advancements in GitHub Copilot CLI

Released alongside the SDK, Copilot CLI has also received major updates, making it an even more powerful development tool:

Enhanced Core Capabilities

  1. Persistent Memory: Cross-session context retention and intelligent compaction
  2. Multi-Model Collaboration: Choose different models for explore, plan, and review workflows
  3. Autonomous Execution:
    • Custom agent support
    • Agent skill system
    • Full MCP support
    • Async task delegation

Real-World Applications

Development teams have already built innovative applications using the SDK:

  • YouTube chapter generators
  • Custom GUI interfaces for agents
  • Speech-to-command workflows for desktop apps
  • Games where you compete with AI
  • Content summarization tools

These examples showcase the flexibility and power of the Copilot SDK.

SDK vs CLI: Complementary, Not Competing

Understanding the relationship between SDK and CLI is important:

  • CLI: An interactive tool for end users, providing a complete development experience
  • SDK: A programmable layer for developers to build customized applications

The SDK essentially provides programmatic access to the CLI's core capabilities, enabling developers to:

  • Integrate Copilot agent capabilities into any environment
  • Build graphical user interfaces
  • Create personal productivity tools
  • Run custom internal agents in enterprise workflows

GitHub handles the underlying authentication, model management, MCP servers, and session management, while developers focus on building value on top of these building blocks.

Best Practices and Recommendations

Based on experience from the agent-framework-update-everyday project, here are practical recommendations:

1. Leverage Copilot Skills Effectively

Define clear skill files that specify:

  • Input and output formats for tasks
  • Rules for handling edge cases
  • Quality standards and priorities

2. Choose Models Wisely

Use different models for different tasks:

  • Exploratory tasks: Use more powerful models (e.g., GPT-5)
  • Execution tasks: Use faster models (e.g., Claude Sonnet)
  • Cost-sensitive tasks: Balance performance and budget

3. Implement Robust Error Handling

AI calls in CI/CD environments need to consider:

  • Network timeout and retry strategies
  • API rate limit handling
  • Output validation and fallback mechanisms

4. Secure Authentication Management

Use fine-grained Personal Access Tokens (PAT):

  • Create dedicated Copilot access tokens
  • Set minimum permission scope (Copilot Requests: Read)
  • Store securely using GitHub Secrets

5. Version Control and Traceability

Automated systems should:

  • Log metadata for each execution
  • Preserve historical outputs for comparison
  • Implement auditable change tracking

Future Outlook

The release of GitHub Copilot SDK marks the democratization of AI agent development. Developers can now:

  1. Lower Development Barriers: No need to deeply understand complex AI infrastructure
  2. Accelerate Innovation: Focus on business logic rather than underlying implementation
  3. Flexible Integration: Embed AI capabilities into any application scenario
  4. Production-Ready: Leverage proven execution loops and security mechanisms

As the SDK moves from technical preview to general availability, we can expect:

  • Official support for more languages
  • Richer tool ecosystem
  • More powerful MCP integration capabilities
  • Community-driven best practice libraries

Conclusion

This article demonstrates how to build practical automation systems using GitHub Copilot SDK through the agent-framework-update-everyday project. This case study not only validates the SDK's technical capabilities but, more importantly, showcases a new development paradigm:

Using AI agents as programmable building blocks, integrated into daily development workflows, to liberate developer creativity.

Whether you want to build personal productivity tools, enterprise internal agents, or innovative AI applications, the Copilot SDK provides a solid technical foundation. Visit github/copilot-sdk to start your AI agent journey today!

Reference Resources

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

Engineering a Local-First Agentic Podcast Studio: A Deep Dive into Multi-Agent Orchestration

1 Share

The transition from standalone Large Language Models (LLMs) to Agentic Orchestration marks the next frontier in AI development. We are moving away from simple "prompt-and-response" cycles toward a paradigm where specialized, autonomous units—AI Agents—collaborate to solve complex, multi-step problems. As a Technology Evangelist, my focus is on building these production-grade systems entirely on the edge, ensuring privacy, speed, and cost-efficiency.

This technical guide explores the architecture and implementation of The AI Podcast Studio. This project demonstrates the seamless integration of the Microsoft Agent FrameworkLocal Small Language Models (SLMs), and VibeVoice to automate a complete tech podcast pipeline.

I. The Strategic Intelligence Layer: Why Local-First?

At the core of our studio is a Local-First philosophy. While cloud-based LLMs are powerful, they introduce friction in high-frequency, creative pipelines. By using Ollama as a model manager, we run SLMs like Qwen-3-8B directly on user hardware.

1. Architectural Comparison: Local vs. Cloud

Choosing the deployment environment is a fundamental architectural decision. For an agentic podcasting workflow, the edge offers distinct advantages:

DimensionLocal Models (e.g., Qwen-3-8B)Cloud Models (e.g., GPT-4o)
LatencyZero/Ultra-low: Instant token generation without network "jitter".Variable: Dependent on network stability and API traffic.
PrivacyTotal Sovereignty: Creative data and drafts never leave the local device.Shared Risk: Data is processed on third-party servers.
CostZero API Fees: One-time hardware investment; free to run infinite tokens.Pay-as-you-go: Costs scale with token count and frequency of calls.
AvailabilityOffline: The studio remains functional without an internet connection.Online Only: Requires a stable, high-speed connection.

2. Reasoning and Tool-Calling on the Edge

To move beyond simple chat, we implement Reasoning Mode, utilizing Chain-of-Thought (CoT) prompting. This allows our local agents to "think" through the podcast structure before writing. Furthermore, we grant them "superpowers" through Tool-Calling, allowing them to execute Python functions for real-time web searches to gather the latest news.

II. The Orchestration Engine: Microsoft Agent Framework

The true complexity of this project lies in Agent Orchestration—the coordination of specialized agents to work as a cohesive team. We distinguish between Agents, who act as "Jazz Musicians" making flexible decisions, and Workflows, which act as the "Orchestra" following a predefined score.

1. Advanced Orchestration Patterns

Drawing from the WorkshopForAgentic architecture, the studio utilizes several sophisticated patterns:

  • Sequential: A strict pipeline where the output of the Researcher flows into the Scriptwriter.
  • Concurrent (Parallel): Multiple agents search different news sources simultaneously to speed up data gathering.
  • Handoff: An agent dynamically "transfers" control to another specialist based on the context of the task.
  • Magentic-One: A high-level "Manager" agent decides which specialist should handle the next task in real-time.

III. Implementation: Code Analysis (Workshop Patterns)

To maintain a production-grade codebase, we follow the modular structure found in the WorkshopForAgentic/code directory. This ensures that agents, clients, and workflows are decoupled and maintainable.

1. Configuration: Connecting to Local SLMs

The first step is initializing the local model client using the framework's Ollama integration.

# Based on WorkshopForAgentic/code/config.py from agent_framework.ollama import OllamaChatClient # Initialize the local client for Qwen-3-8B # Standard Ollama endpoint on localhost chat_client = OllamaChatClient( model_id="qwen3:8b", endpoint="http://localhost:11434" )

 

2. Agent Definition: Specialized Roles

Each agent is a ChatAgent instance defined by its persona and instructions.

# Based on WorkshopForAgentic/code/agents.py from agent_framework import ChatAgent # The Researcher Agent: Responsible for web discovery researcher_agent = client.create_agent( name="SearchAgent", instructions="You are my assistant. Answer the questions based on the search engine.", tools=[web_search], ) # The Scriptwriter Agent: Responsible for conversational narrative generate_script_agent = client.create_agent( name="GenerateScriptAgent", instructions=""" You are my podcast script generation assistant. Please generate a 10-minute Chinese podcast script based on the provided content. The podcast script should be co-hosted by Lucy (the host) and Ken (the expert). The script content should be generated based on the input, and the final output format should be as follows: Speaker 1: …… Speaker 2: …… Speaker 1: …… Speaker 2: …… Speaker 1: …… Speaker 2: …… """ )

3. Workflow Setup: The Sequential Pipeline

For a deterministic production line, we use the WorkflowBuilder to connect our agents.

# Based on WorkshopForAgentic/code/workflow_setup.py from agent_framework import WorkflowBuilder # Building the podcast pipeline search_executor = AgentExecutor(agent=search_agent, id="search_executor") gen_script_executor = AgentExecutor(agent=gen_script_agent, id="gen_script_executor") review_executor = ReviewExecutor(id="review_executor", genscript_agent_id="gen_script_executor") # Build workflow with approval loop # search_executor -> gen_script_executor -> review_executor # If not approved, review_executor -> gen_script_executor (loop back) workflow = ( WorkflowBuilder() .set_start_executor(search_executor) .add_edge(search_executor, gen_script_executor) .add_edge(gen_script_executor, review_executor) .add_edge(review_executor, gen_script_executor) # Loop back for regeneration .build() )

 

IV. Multimodal Synthesis: VibeVoice Technology

The "Future Bytes" podcast is brought to life using VibeVoice, a specialized technology from Microsoft Research designed for natural conversational synthesis.

  • Conversational Rhythm: It automatically handles natural turn-taking and speech cadences.
  • High Efficiency: By operating at an ultra-low 7.5 Hz frame rate, it significantly reduces the compute power required for high-fidelity audio.
  • Scalability: The system supports up to 4 distinct voices and can generate up to 90 minutes of continuous audio.

V. Observability and Debugging: DevUI

Building multi-agent systems requires deep visibility into the agentic "thinking" process. We leverage DevUI, a specialized web interface for testing and tracing:

  • Interactive Tracing: Developers can watch the message flow and tool-calling in real-time.
  • Automatic Discovery: DevUI auto-discovers agents defined within the project structure.
  • Input Auto-Generation: The UI generates input fields based on workflow requirements, allowing for rapid iteration.

VI. Technical Requirements for Edge Deployment

Deploying this studio locally requires specific hardware and software configurations to handle simultaneous LLM and TTS inference:

  • Software: Python 3.10+, Ollama, and the Microsoft Agent Framework.
  • Hardware16GB+ RAM is the minimum requirement; 32GB is recommended for running multiple agents and VibeVoice concurrently.
  • Compute: A modern GPU/NPU (e.g., NVIDIA RTX or Snapdragon X Elite) is essential for smooth inference.

Final Perspective: From Coding to Directing

The AI Podcast Studio represents a significant shift toward Agentic Content Creation. By mastering these orchestration patterns and leveraging local EdgeAI, developers move from simply writing code to directing entire ecosystems of intelligent agents. This "local-first" model ensures that the future of creativity is private, efficient, and infinitely scalable.

Download sample Here

Resource

  1. EdgeAI for Beginners - https://github.com/microsoft/edgeai-for-beginners
  2. Microsoft Agent Framework - https://github.com/microsoft/agent-framework
  3. Microsoft Agent Framework Samples - https://github.com/microsoft/agent-framework-samples
Read the whole story
alvinashcraft
21 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

#713 – Rubber Duck Incarnate

1 Share





Download audio: https://traffic.libsyn.com/theamphour/TheAmpHour-713-RubberDuckIncarnate.mp3
Read the whole story
alvinashcraft
21 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories