UNPKG

agentfootprint

Version:

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

79 lines • 4 kB
/** * requestSealing — a host hands each run the credential that arrived with ITS * request, sealed for that run alone. * * `requestContext({ secrets })` (`agentfootprint/security`) seals a request's * secrets; `requestCredentials` vends them to the tools that declare a `needs` * for them. A host that runs the agent for you (`standingAgent`, * `streamHandler`) cannot know which header holds the credential, so it asks * the app — once per request, with the request's headers — through the * {@link RequestSealer} option `sealRequest`, and passes the handle to that * request's `run()` / `resume()` and nowhere else. The sealer is handed the * headers ONLY: nothing about who is calling, so it can never be tempted to * look a stored credential up by a name the caller could have claimed. * * THE RULES, in one funnel ({@link requestSealing}): * - asked only for a request that runs, after the caller is bound and every * refusal is behind it — a refused request never seals anything; * - the answer is a handle `requestContext` built, or `undefined` (no * request: a tool that needs one fails closed, by name); * - ONE HANDLE, ONE RUN, PROCESS-WIDE: a handle any host was handed before * is refused, so a sealer that caches one — even one shared by two doors * — cannot hand one caller's credential to another caller's run; * - a sealer that throws refuses the request in FIXED words * (`ERR_REQUEST_NOT_SEALED`, 400). Its own message is never sent: it may * quote the value it could not read. * The handle never enters a frame, a stored session or a checkpoint; the * headers never leave the sealer. * * Pattern: guarded strategy call. Role: hosting, run input. */ import { isRequestContext } from '../identity/requestContext.js'; import { WireRequestRefusal } from './errors.js'; /** The code a request refused by its sealer carries — the status is 400. */ export const REQUEST_NOT_SEALED = 'ERR_REQUEST_NOT_SEALED'; /** * Every handle any host in this process handed to a run — held weakly, * compared by identity. Module-wide on purpose: two doors sharing one caching * sealer must not each accept the same handle once. */ const handedToARun = new WeakSet(); /** * The one funnel a host seals through: the app's sealer, checked. `undefined` * when no sealer was given — the host passes no request, exactly as before. * * @throws TypeError at construction when `sealer` is not a function. */ export function requestSealing(sealer, door) { if (sealer === undefined) return undefined; if (typeof sealer !== 'function') { throw new TypeError(`[hosting] '${door}': sealRequest must be a function that returns ` + 'requestContext({ secrets }) for one request, or undefined.'); } return async (facts) => { let handle; try { handle = await sealer(facts); } catch { // Never the sealer's message: it may quote the value it could not read. throw new WireRequestRefusal(REQUEST_NOT_SEALED, `'${door}': the credentials this request carries could not be sealed for its run.`, 400); } if (handle === undefined) return undefined; if (!isRequestContext(handle)) { throw new TypeError(`[hosting] '${door}': sealRequest must return a handle built by ` + 'requestContext({ secrets }), or undefined — a plain object is refused, because its ' + 'values would travel wherever the run options are logged or copied.'); } if (handedToARun.has(handle)) { throw new TypeError(`[hosting] '${door}': sealRequest returned a handle it returned before. Every ` + "request seals its own — a shared handle hands one caller's credential to another " + "caller's run."); } handedToARun.add(handle); return handle; }; } //# sourceMappingURL=requestSealing.js.map