pks aspire

beta

Start a .NET Aspire AppHost with its declared parameters already resolved from what you are signed in to, instead of pasting endpoints and keys into prompts.

Author: Poul Kjeldager
Usage: pks aspire <run|init> [options] [-- <apphost args>]
Category: infrastructure

Examples

$ pks aspire init src/apphost

Reference the declare package from an AppHost so it can say what it needs

$ pks aspire run -- --ai

Start the AppHost, forwarding --ai to it, with its parameters resolved

$ pks aspire run --dry-run -- --ai

Show what would be set, and start nothing

pks aspire runs a .NET Aspire AppHost without anybody typing a credential into it.

An AppHost that needs a model endpoint, a key and a deployment name declares them as parameters, and Aspire's honest answer is to stop and ask — every run, or once into user secrets, where the key then lives in plaintext on a laptop for as long as the project does. pks aspire run removes the question: the composition says what kind of thing it needs, and pks fills it from whatever this machine is already signed in to.

It is the same two-phase handshake as pks exec, with the AppHost's pipeline standing in for PKS_DISCOVERY=1.

The two passes

  1. Declare. aspire do pks-declare builds the AppHost and executes one pipeline step. The step has no dependencies and no resources behind it, so nothing starts — no container, no app, no port. It walks the resource model and writes a v1 manifest to the file named by PKS_DECLARE_OUT.
  2. Run. pks resolves the manifest — provider per capability, model per role, placeholders expanded — and starts aspire run with the answers in its environment as Parameters__<name>, which is the first place Aspire looks. The parameters resolve silently and nothing is written to disk.

An AppHost without the step still works. The first pass fails, pks aspire run says so, and the run continues exactly as aspire run would have.

The reminder

An AppHost that carries the declare step and is started with plain aspire run puts a dismissible message bar on the dashboard: Started without pks — this AppHost can fill its own parameters. The run works; it just stops and asks for the values pks would have supplied, and Aspire's prompt cannot say why it appeared. The bar can.

pks aspire run sets PKS_ASPIRE_RUN=1 on the run it starts, and the reminder stays away. So does a publish, the declare pass itself, and any host with no dashboard client — which is what keeps it out of CI and out of Aspire.Hosting.Testing. PKS_ASPIRE_NO_REMINDER=1 silences it on a machine that has no pks and does not want one.

The logic lives in Agentics.Extensions.Aspire.Declare, so an AppHost wired before this existed gets the bar when it moves to the package (or after pks aspire init --source --force if it still carries the copy).

Commands

pks aspire init [APPHOST]

Adds a reference to Agentics.Extensions.Aspire.Declaredotnet add package, no version pinned, so a fresh init stays correct as the package moves. The package is a plain Aspire extension: it does not reference pks-cli and does not talk to it, so the AppHost still builds and runs on a machine that has never heard of pks.

A single-file apphost.cs has no project to add to; the command prints the #:package line to paste instead. --source writes AgenticsDeclare.cs into the project for an AppHost that genuinely cannot take the package — the same source the package is built from, so the two cannot drift; --force replaces an existing copy.

This shipped as a copied file until 2026-08-29. An AppHost still carrying PksDeclare.cs must delete it before referencing the package — otherwise SuggestedValue is defined twice and the step is registered twice — and rename its call sites: AddPksDeclareAddAgenticsDeclare, AddPksCapabilityAddAgenticsCapability, PksDeclareExtensionsAgenticsDeclareExtensions. The command detects the leftover file and says so.

APPHOST is a project file or a directory containing exactly one. A directory with several is an error rather than a guess.

pks aspire run [-- <apphost args>]

OptionMeaning
--apphost <PATH>Project file or directory, passed through to aspire
--provider <KIND>Skip the provider prompt: foundry, gemini, openai-compatible
--port <N>Bind the managed-identity proxy to a fixed port
--non-interactiveTake the default for every question — for CI
--dry-runDeclare, resolve, print what would be set, start nothing
--startUse aspire start (detached) rather than the foreground aspire run

Everything after -- goes to the AppHost — and to both passes. This matters: an AppHost that only declares its model parameters behind --ai will declare nothing if the first pass does not get the flag too.

Declaring, in the AppHost

var aiBaseUrl = builder.AddParameter("ai-base-url");
var aiApiKey  = builder.AddParameter("ai-api-key", secret: true);
var aiModel   = builder.AddParameter("ai-model", new SuggestedValue("gpt-4o-mini"));

builder.AddAgenticsCapability("chat", "The model that writes the answer on Overview")
       .Offers("foundry", "Azure AI Foundry — sign in once with `pks foundry init`")
       .Offers("openai-compatible", "Anything OpenAI-compatible: a local Ollama, a proxy")
       .Binds(aiBaseUrl, "{endpoint:openai}")
       .Binds(aiApiKey,  "{apikey}")
       .Binds(aiModel,   "{model:default}");

Offers names the kinds of provider that could fill the capability; pks shows only the ones you are signed in to. Binds sends one resolved value into one parameter.

Placeholder vocabulary

PlaceholderResolves to
{endpoint}The chosen provider's endpoint URL
{endpoint:openai}The same, in the shape an OpenAI client can be pointed at
{apikey}A key for that endpoint, where the provider has one
{model:<role>}The model chosen for a named role
{imds:endpoint}A loopback managed-identity proxy pks starts for the run
{imds:header}That proxy's per-run secret
{entra:tenantid}The tenant of an app registration pks entra app init provisioned
{entra:clientid}Its client id
{entra:clientsecret}Its client secret, from the encrypted store

An {entra:…} placeholder takes the alias from the capability's own name; {entra:clientid:other} names a different one, which is how one composition binds two registrations.

When the alias is not stored and the run is interactive, pks offers to take the tenant id, client id and secret right there, and asks afterwards whether to keep them — defaulting to no, in which case they exist only for this run. Aspire's own dialog would ask too, but its Save to user secret checkbox writes plaintext under the project; this one forgets. --non-interactive skips the offer entirely.

Anything else passes through as a literal. A role is discovered from the bindings — binding {model:default} is how the composition says there is a role called default to ask about.

A capability is optional by default. AddAgenticsCapability(name, description, required: true) makes a missing provider stop the run instead of skipping the capability.

What gets reported

The manifest carries every parameter in the model, not only the bound ones, with whether pks can fill it and whether it already has an answer. pks aspire run prints the ones that are neither — a tenant id, a connection string — so a run that is about to stop and ask says so up front rather than after the build.

Values are never in the manifest. Names, descriptions and two booleans; the file lands on disk, and a value there would be a credential on disk.

Traps

  • AddParameter("ai-model", "gpt-4o-mini") cannot be filled by anything. The string overload pins the value and stops consulting configuration, so the environment variable is ignored without a word and the parameter keeps its old value while everything reports success. Use new SuggestedValue("gpt-4o-mini"), which is the default the reading suggests.
  • The declare pass runs in publish mode. aspire do always does, so an AppHost that branches on ExecutionContext.IsPublishMode — a Key Vault instead of parameters, a real tenant instead of an emulator — describes the deployment while the run that follows is a local one. The parameters missing from the manifest are then exactly the ones pks was asked to fill, and the symptom is a run that resolves nothing and reports nothing wrong. The package exposes AgenticsDeclareExtensions.IsDeclaring for this: write builder.ExecutionContext.IsPublishMode && !declaring, which reads as "actually publishing", and the declare pass then sees the composition the run will have.
  • No shell can export Parameters__ai-base-url. A dash is not a legal variable name to bash or zsh. pks sets it on the child process directly, which works; a hand-written export does not.

See also

  • pks exec — the same handshake for a command-line tool
  • pks foundry — signing in, so there is something to resolve against