agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
189 lines • 10.3 kB
TypeScript
/**
* entraIdentity — the {@link CredentialProvider} port over Microsoft Entra ID
* (peer-dep `@azure/identity`).
*
* import { entraIdentity } from 'agentfootprint/security';
* const credentials = entraIdentity();
*
* ── What it is, and what it deliberately is not ─────────────────────────────
* This is the **narrow** adapter: it vends *Entra* access tokens for *Azure*
* APIs, from whatever credential the environment already has — the
* DefaultAzureCredential chain walks environment service principal, workload
* identity, managed identity, VS Code, Azure CLI, Azure PowerShell and the
* Azure Developer CLI, in that order. That is one job and it is done
* completely.
*
* It is **not** a user-delegation surface. Entra's on-behalf-of flow (and any
* 3-legged consent dance) needs a confidential client app registration that
* this adapter does not hold, so `mode: 'user'` is **refused by name** rather
* than quietly served with a machine token. A machine token returned where a
* user token was asked for is the exact silent downgrade the port exists to
* prevent: the call succeeds, the data comes back, and it was the agent's
* access rather than the person's. OBO is a later train; when it lands it will
* be its own provider, not a flag here.
*
* ── The audience split, and where it bites ──────────────────────────────────
* Azure tokens are minted for ONE audience. {@link AZURE_AI_SCOPE}
* (`https://ai.azure.com/.default`) is the data plane — every Foundry and
* Azure OpenAI inference call takes it. {@link AZURE_MANAGEMENT_SCOPE}
* (`https://management.azure.com/.default`) is the ARM control plane —
* listing deployments, creating resources. A token for one audience is a 401
* on the other, which is why BOTH are exported by name instead of leaving the
* caller to guess a string. The default here is the data-plane scope, because
* vending inference credentials is what an agent runtime does all day.
*
* ── Caching: the credential, never a token ──────────────────────────────────
* One `DefaultAzureCredential` is constructed for the life of the provider and
* every `getToken` call goes through it. MSAL — the machinery underneath
* `@azure/identity` — caches and proactively refreshes tokens internally, so
* caching a token HERE would mean owning an expiry this adapter did not
* compute and cannot see revoked. Unlike the Google adapter (where scopes are
* fixed at client construction and a different scope set needs its own
* client), Azure scopes travel per `getToken` call, so the ONE cached
* credential serves every scope set.
*
* ── Secrets ─────────────────────────────────────────────────────────────────
* The `sdkFailure` law, same as every other credential-touching adapter here:
* the library's own message never comes through, because auth libraries echo
* request detail into 401/403 text and a message thrown from a
* `CredentialProvider` reaches the LLM as a tool result AND rides
* `agentfootprint.credential.failed` to every sink. What comes through is the
* operation that failed and the error's NAME. The original is not attached as
* `cause` — a cause travels into every serializer that walks own properties,
* which would undo all of it in one `JSON.stringify`.
*
* Pattern: Adapter (GoF) + lazy peer-dep load — `@azure/identity` is required
* the first time `getCredential` runs, or never if you inject a credential.
*/
import type { CredentialProvider } from '../../identity/types.js';
/**
* The data-plane scope for ALL Foundry / Azure OpenAI inference
* (`https://ai.azure.com/.default`). This is the default scope this provider
* requests.
*
* The audience split matters: a token minted for this scope does NOT work on
* the ARM control plane, and a {@link AZURE_MANAGEMENT_SCOPE} token does not
* work here — Azure validates the audience on every call. Both are exported by
* name so nobody has to remember which string is which.
*/
export declare const AZURE_AI_SCOPE = "https://ai.azure.com/.default";
/**
* The ARM control-plane scope (`https://management.azure.com/.default`) —
* listing deployments, managing resources. A DIFFERENT audience from
* {@link AZURE_AI_SCOPE}: a token for one is a 401 on the other, which is why
* both are named rather than leaving the caller to guess.
*/
export declare const AZURE_MANAGEMENT_SCOPE = "https://management.azure.com/.default";
/**
* The CLASSIC Azure OpenAI data-plane scope
* (`https://cognitiveservices.azure.com/.default`) — the audience Microsoft's
* own keyless guidance names for the older deployment-scoped route
* (`{endpoint}/openai/deployments/{d}/…`), which is the route `azureOpenai()`
* builds and therefore its default. Current resources widely accept
* {@link AZURE_AI_SCOPE} too, but an older `*.openai.azure.com` resource may
* not — and a door should default to the audience ITS route documents, not the
* one its sibling uses. (Azure Government spells this
* `https://cognitiveservices.azure.us/.default`.)
*/
export declare const AZURE_COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default";
/**
* The `@azure/core-auth` `TokenCredential` duck type — the slice this adapter
* calls. Anything `@azure/identity` exports (DefaultAzureCredential,
* ManagedIdentityCredential, ClientSecretCredential, …) satisfies it.
*
* These are THE shared Azure credential duck-types for the whole repo: sibling
* Azure adapters `import type` them from this file rather than re-declaring
* their own spelling of the same SDK surface.
*/
export interface TokenCredentialLike {
/**
* Mint (or serve from MSAL's internal cache) an access token for the given
* scope(s). May resolve to `null` — the SDK's spelling of "no token
* available" — which this adapter refuses by name rather than passing along.
*/
getToken(scopes: string | readonly string[], options?: unknown): Promise<AccessTokenLike | null>;
}
/**
* The `@azure/core-auth` `AccessToken` duck type. `expiresOnTimestamp` is unix
* MILLISECONDS — the port reports unix SECONDS, and the conversion lives in
* exactly one place ({@link entraIdentity}'s vend path).
*
* Shared repo-wide alongside {@link TokenCredentialLike} — sibling Azure
* adapters `import type` it from here.
*/
export interface AccessTokenLike {
/** The bearer token itself. A SECRET — never echoed, never logged. */
readonly token: string;
/** Expiry in unix MILLISECONDS epoch (the SDK's unit, not the port's). */
readonly expiresOnTimestamp: number;
/** MSAL's proactive-refresh hint, when the SDK provides one. */
readonly refreshAfterTimestamp?: number;
/** 'Bearer' | 'pop'; absent on older SDK versions. */
readonly tokenType?: string;
}
/** The slice of `@azure/identity` this adapter loads. */
export interface AzureIdentitySdkModule {
readonly DefaultAzureCredential?: new () => TokenCredentialLike;
}
/** Options for {@link entraIdentity}. */
export interface EntraIdentityOptions {
/**
* The scopes to request. Default `[AZURE_AI_SCOPE]` — the data-plane
* audience every Foundry / Azure OpenAI inference call accepts.
*
* A request's own `scopes` win when it names any — a tool that knows it
* needs the control plane says so, and this is where that is honoured.
*/
readonly scopes?: readonly string[];
/**
* Which downstream services this provider will answer for.
*
* Unset — the default — it answers for ANY `service`, because the token it
* vends is an Entra credential and the caller knows better than this
* adapter which Azure API they are about to call.
*
* Set it and a request for a service outside the list is refused BY NAME
* rather than served. That is the useful setting in a deployment where
* tools declare `needs: [{ credential: 'github' }]` alongside Azure ones:
* without it, this provider would happily hand an Entra access token to the
* tool that wanted a GitHub one, and the failure would surface as a
* puzzling 401 from GitHub rather than as a wiring error here.
*/
readonly services?: readonly string[];
/** Stable provider id (default `'entra-identity'`). */
readonly id?: string;
/**
* @internal Test seam — a pre-built credential. Bypasses the SDK entirely,
* so the suite runs with no package and no Azure account.
*/
readonly _credential?: TokenCredentialLike;
/** @internal Test seam — the SDK module, to exercise the real construction. */
readonly _sdk?: AzureIdentitySdkModule;
}
/**
* Vend Entra access tokens from whatever credential this environment has —
* the DefaultAzureCredential chain: environment service principal, workload
* identity, managed identity, VS Code, Azure CLI, Azure PowerShell, Azure
* Developer CLI. (`AZURE_TOKEN_CREDENTIALS` can restrict the chain; that is
* the SDK's own dial and this adapter does not second-guess it.)
*
* @throws when `mode: 'user'` is requested — no user-delegation surface is
* wired for Entra yet (on-behalf-of is a later train), and a machine token
* returned in its place would be a silent downgrade.
* @throws when `services` is configured and the request names another one.
*
* @example A tool that calls an Azure API with the deployment's own identity
* const agent = Agent.create({ provider, credentials: entraIdentity() })
* .tool(defineTool({
* name: 'ask_foundry',
* needs: [{ credential: 'azure-ai' }],
* execute: async (args, ctx) =>
* fetch(url, { headers: ctx.credential!.toHeaders() }).then((r) => r.text()),
* }))
* .build();
*
* @example A control-plane token, without touching the data-plane default
* entraIdentity({ scopes: [AZURE_MANAGEMENT_SCOPE] });
*/
export declare function entraIdentity(options?: EntraIdentityOptions): CredentialProvider;
//# sourceMappingURL=azure.d.ts.map