UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

165 lines • 7.76 kB
"use strict"; /** * requestCredentials — a {@link CredentialProvider} fed from the INCOMING * REQUEST: the credential a tool uses is the one this run's caller sent. * * ```ts * import { requestCredentials, requestContext, decodedWith } from 'agentfootprint/security'; * * // Once, at boot — no secret in here: * const credentials = requestCredentials({ * services: { * crm: 'crmToken', // bearer, as sent * billing: { secret: 'billingKey', as: asApiKey('x-api-key') }, * mcp: { secret: 'mcpToken', as: decodedWith((raw) => decrypt(raw)) }, * }, * }); * const agent = Agent.create({ provider, credentials }).tool(crmTool).build(); * * // Per request: * await agent.run({ message }, { request: requestContext({ secrets: { crmToken, mcpToken } }) }); * ``` * * A tool declares `needs: { credential: 'crm' }` and reads `ctx.credential`, * exactly as with any other provider. Nothing about the tool changes. * * ── Interface → adapter → strategies ──────────────────────────────────────── * The interface is the `CredentialProvider` port. This adapter reads the * secret named for the service from the request the call belongs to * (`CredentialRequest.request`, filled by the framework). A strategy turns the * raw value into a `Credential`: {@link asBearer} (the default), * {@link asApiKey}, or {@link decodedWith} for a value that must be decrypted * or exchanged first. * * ── Fails closed, by name ─────────────────────────────────────────────────── * Every refusal is a {@link RequestCredentialError} naming the SERVICE and the * secret's NAME — never a value, and never a strategy's own error text (a * decrypt library may quote its input). A thrown message reaches the model as * the tool result and rides `agentfootprint.credential.failed`. * - no request on the call (a run started without one, a mount no run * reaches, a connection-level caller) → `'no-request'`; * - the request does not carry the secret → `'missing'`; * - a service this provider was not told about → `'unknown-service'`; * - the strategy threw or returned no credential → `'undecodable'`. * There is no fallback: a missing per-request credential is never served from * another request, a default, or the process. * * Pattern: Adapter (port) + Strategy (raw value → Credential). */ Object.defineProperty(exports, "__esModule", { value: true }); exports.requestCredentials = exports.decodedWith = exports.asApiKey = exports.asBearer = exports.RequestCredentialError = void 0; const kinds_js_1 = require("./kinds.js"); const requestContext_js_1 = require("./requestContext.js"); /** * A request credential that could not be served. Its message names the * service and the secret's name; it never carries a value or a strategy's * own error. */ class RequestCredentialError extends Error { reason; service; code = 'ERR_REQUEST_CREDENTIAL'; constructor(reason, service, message) { super(message); this.reason = reason; this.service = service; this.name = 'RequestCredentialError'; } } exports.RequestCredentialError = RequestCredentialError; /** The raw value, as a bearer token (`Authorization: Bearer …`). The default. */ function asBearer() { return (raw) => (0, kinds_js_1.bearer)(raw); } exports.asBearer = asBearer; /** The raw value, as an API key in one header (default `x-api-key`). */ function asApiKey(headerName) { return (raw) => (0, kinds_js_1.apiKey)(raw, headerName); } exports.asApiKey = asApiKey; /** * Decode the raw value first — decrypt an encrypted per-request token, * exchange it — then read the result with `then` (default {@link asBearer}). * `decode` may return the plain value or a finished `Credential`. Whatever it * throws is replaced by an `'undecodable'` refusal that quotes nothing. */ function decodedWith(decode, then = asBearer()) { return async (raw) => { const decoded = await decode(raw); return typeof decoded === 'string' ? then(decoded) : decoded; }; } exports.decodedWith = decodedWith; /** * Build the provider. Configuration is checked here, at boot: an empty * service map, an empty secret name or a strategy that is not a function is * refused by name. * * @throws TypeError on a malformed `services` map. */ function requestCredentials(options) { const sources = readSources(options?.services); return { id: options.id ?? 'request-credentials', async getCredential(req) { const service = req.service; const source = Object.hasOwn(sources, service) ? sources[service] : undefined; if (source === undefined) { throw new RequestCredentialError('unknown-service', service, `requestCredentials: no request credential is declared for service '${service}'.`); } if (req.request === undefined) { throw new RequestCredentialError('no-request', service, `requestCredentials: service '${service}' takes its credential from the incoming ` + 'request, and the call carries none — pass `request: requestContext({ secrets })` ' + 'to the run. A credential is never borrowed from another request.'); } const raw = (0, requestContext_js_1.requestSecretOf)(req.request, source.secret); if (raw === undefined) { throw new RequestCredentialError('missing', service, `requestCredentials: the request carries no '${source.secret}' secret for service ` + `'${service}'.`); } let credential; try { credential = await source.as(raw); } catch { credential = undefined; } if (!isCredential(credential)) { throw new RequestCredentialError('undecodable', service, `requestCredentials: the '${source.secret}' secret for service '${service}' could ` + 'not be read as a credential.'); } return { status: 'issued', credential }; }, }; } exports.requestCredentials = requestCredentials; function readSources(services) { if (services === null || typeof services !== 'object' || Array.isArray(services)) { throw new TypeError('requestCredentials: `services` must be an object of service → source.'); } const out = Object.create(null); const names = Object.keys(services); if (names.length === 0) { throw new TypeError('requestCredentials: `services` names no service.'); } for (const service of names) { const entry = services[service]; const secret = typeof entry === 'string' ? entry : entry?.secret; const as = typeof entry === 'string' ? undefined : entry?.as; if (typeof secret !== 'string' || secret === '') { throw new TypeError(`requestCredentials: service '${service}' must name its secret (a non-empty string).`); } if (as !== undefined && typeof as !== 'function') { throw new TypeError(`requestCredentials: service '${service}': \`as\` must be a strategy function.`); } out[service] = { secret, as: as ?? asBearer() }; } return out; } function isCredential(value) { return (typeof value === 'object' && value !== null && typeof value.kind === 'string' && typeof value.toHeaders === 'function'); } //# sourceMappingURL=requestCredentials.js.map