agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
171 lines • 9.2 kB
TypeScript
/**
* identity — the CredentialProvider port.
*
* OUTBOUND auth: vend a credential/token so a tool can call a downstream service
* (GitHub, Slack, Google…) on behalf of the agent or the end user. This is
* DISTINCT from `agentfootprint/security` (authorization — "is this tool
* allowed"); identity answers "get me a token to call X".
*
* Pattern: Port (Hexagonal). Vendors plug in as adapters:
* - `agentCoreIdentity()` — AWS Bedrock AgentCore Identity (token vault + OAuth)
* - `staticTokens()` — dev/test (canned tokens, no network)
*
* Two flows, mirroring OAuth (and AgentCore's `M2M` vs `USER_FEDERATION`):
* - `mode: 'machine'` (2-legged) — client-credentials; returns a token directly.
* - `mode: 'user'` (3-legged) — user-delegated; may need consent. When it
* does, the provider returns `authorization-required` with a URL; the agent
* surfaces it to the human (e.g. via pause/resume) and retries after consent.
* (Most calls skip consent — providers cache refresh tokens.)
*
* **Security invariant:** a vended token is a SECRET. Callers MUST use it locally
* (e.g. as an HTTP header inside a tool's `execute`) and MUST NOT write it to
* tracked scope (`setValue`) — tracked writes flow to the commit log, recorders,
* and observability exporters, which would leak the token into the trace. Pair
* with `RedactionPolicy` for defence in depth.
*/
import type { RequestContext } from './requestContext.js';
/** What a tool/agent asks for. `service` ↔ the provider's downstream service id. */
export interface CredentialRequest {
/** Downstream service id, e.g. 'github', 'slack', 'google'. */
readonly service: string;
/** OAuth scopes to request. */
readonly scopes?: readonly string[];
/** `machine` = 2-legged (M2M); `user` = 3-legged (on behalf of a user). Default `machine`. */
readonly mode?: 'machine' | 'user';
/** The principal/tenant the token is for (the agent + end-user identity). */
readonly identity?: {
readonly principal?: string;
readonly tenant?: string;
};
/**
* The end user's OWN token — the JWT their identity provider issued them,
* as it arrived at your door (9.12.0).
*
* **The difference between an assertion and a proof.** `identity.principal`
* is a string this process wrote down: an agent that really did authenticate
* a person can say so, and a provider has to take its word for it. This is
* the artifact the IdP signed, so a provider that can verify it — AWS
* AgentCore Identity exchanges it for a workload token scoped to *that
* user* — vends on the strength of the user's own credential rather than on
* the agent's claim about them.
*
* **Per REQUEST, never per provider.** A token belongs to the person calling
* right now, so it rides here beside `service` and `mode`, not in the
* provider's construction options where one value would serve everybody.
*
* **SECRET, and deliberately not threaded through the framework.** The
* declare-and-push path (`ctx.credentials`) does not fill this in: the only
* places a JWT could travel from the door to a tool are tracked scope and
* the run input, and both flow to the commit log, the recorders and every
* observability exporter. A tool that needs it captures it at the door in its
* own closure and passes it here. See the security invariant at the top of
* this file — it applies to this field with the same force as to a vended
* credential, and a provider that echoes it into an error message has leaked
* the user's session to the model.
*
* Providers that cannot use one MUST ignore it (never echo it, never log it);
* `staticTokens` does exactly that.
*/
readonly userToken?: string;
/**
* The incoming request this call belongs to, sealed — set by the FRAMEWORK
* from the run the call is part of, never by a tool or the model.
*
* Opaque: it carries no readable field. Only `requestCredentials` reads it
* (see `./requestContext`); every other provider ignores it. A call made
* where no run carries a request has none, and a request-sourced credential
* then fails closed.
*/
readonly request?: RequestContext;
/** Force a fresh authorization, bypassing any cached/refresh token. */
readonly forceReauth?: boolean;
/**
* The abort signal of the request this credential is for — present when the
* caller has one, as `gatewayTransport` does for every request it vends for.
*
* A provider that does work of its own (a token endpoint, a vault, an SDK
* call) should hand it on and stop when it fires: the request it would serve
* has been cancelled or ran out of time. Ignoring it is safe on the gateway
* route, never a leak there: `gatewayTransport` stops waiting at the abort
* and never uses a credential that arrives after it, although the provider
* may still finish its own work. That is the gateway's guarantee, not every
* caller's: a direct `withCredentialRetry(...).getCredential(...)` call lets
* an attempt already running settle, and can return the credential it issues
* after the abort. It is a live object, not data: never put it in a cache
* key, a log line or an error message.
*/
readonly signal?: AbortSignal;
}
/**
* A credential, ready to apply to a downstream request. It carries its `kind`
* (so you can read the raw value when you must) AND a **universal applicator**
* `toHeaders()` — the way to USE it without switching on `kind`. Built-in kinds
* (`bearer`/`apiKey`/`basic`/`headers`) live in `./kinds`; a custom kind is any
* object implementing this protocol — so new credential types plug in with no
* library change.
*
* **SECRET.** Use it locally (e.g. `headers: cred.toHeaders()`) inside a tool;
* never write it to tracked scope. It is a live object (carries `toHeaders`),
* so it is intentionally NOT serializable — credentials are used immediately
* and never persisted/traced.
*/
export interface Credential {
/** Discriminator: 'bearer' | 'apiKey' | 'basic' | 'headers' | <your kind>. */
readonly kind: string;
/** The auth header(s) to add to a downstream HTTP request. Universal across kinds. */
toHeaders(): Record<string, string>;
}
/** The provider issued a credential (the 2-legged / refreshed-3-legged happy path). */
export interface CredentialIssued {
readonly status: 'issued';
readonly credential: Credential;
/** Unix seconds when it expires, if known. */
readonly expiresAt?: number;
}
/** 3-legged consent is required: surface `authorizationUrl` to the user, then
* retry `getCredential` after they authorize (`sessionId` correlates the flow). */
export interface CredentialAuthorizationRequired {
readonly status: 'authorization-required';
readonly authorizationUrl: string;
readonly sessionId: string;
}
export type CredentialResult = CredentialIssued | CredentialAuthorizationRequired;
/**
* The port. An adapter implements this against a specific identity backend.
*
* **Security contract for implementers:** a thrown error's `message` is surfaced
* to the LLM (as the tool result) AND emitted on `agentfootprint.credential.failed`
* — so a provider MUST NOT include the secret/token (or an `Authorization` header)
* in thrown error messages. (Some OAuth/HTTP SDKs echo request details into 401/403
* text — scrub those before throwing.) Defence in depth: consumers can also apply
* footprintjs `RedactionPolicy.emitPatterns: [/credential\.failed/]`.
*/
export interface CredentialProvider {
/** Stable id (for logging / "which provider vended this"). */
readonly id: string;
getCredential(req: CredentialRequest): Promise<CredentialResult>;
}
/** Narrow a {@link CredentialResult} to the issued-credential branch. */
export declare function isCredentialIssued(r: CredentialResult): r is CredentialIssued;
/**
* What a tool DECLARES it needs (declare-and-push). The framework resolves this
* with the attached provider BEFORE invoking the tool, and injects the result as
* `ctx.credential`. `credential` is the service id the provider understands.
*/
export interface CredentialNeed {
readonly credential: string;
readonly scopes?: readonly string[];
/** `machine` = 2-legged/M2M; `user` = 3-legged on-behalf-of-user (consent).
* **Omitted → `machine`** — declare `mode: 'user'` explicitly for delegated
* access, or you'll silently get a machine token. */
readonly mode?: 'machine' | 'user';
}
/**
* A fail-closed {@link CredentialProvider} used when none is attached. Every call
* throws loudly — so `ctx.credentials` is never `undefined` (no silent
* optional-chaining bypass), and a tool that needs a credential without one
* configured fails LOUD, not open. Use `ctx.hasCredentials` to branch when a tool
* intentionally supports a no-credential (degraded) mode.
*/
export declare function unconfiguredCredentialProvider(): CredentialProvider;
//# sourceMappingURL=types.d.ts.map