agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
139 lines • 6.71 kB
JavaScript
;
/**
* requestContext — the INCOMING REQUEST's secrets, sealed for one run.
*
* A host that receives a per-request credential (an encrypted token in a
* header, a session-bound key) seals it here and hands the handle to the run:
*
* ```ts
* const request = requestContext({ secrets: { mcpToken: req.headers['x-mcp-token'] } });
* await agent.run({ message }, { request });
* ```
*
* ── Why a sealed handle and not a plain object ──────────────────────────────
* The run's input, its options and its state all travel: input and state into
* the commit log, the recorders and every exporter; options into whatever a
* host logs. A plain `{ mcpToken: '…' }` would ride all of them. The handle
* holds NOTHING an enumeration, `JSON.stringify`, `structuredClone` or an
* inspector can reach — the values live in a module-private `WeakMap` keyed by
* the handle, and only {@link requestSecretOf} (internal, never on a door)
* reads them. So the handle can sit in an options bag, be logged by accident,
* or be copied, and nothing secret moves with it.
*
* ── Who reads it ─────────────────────────────────────────────────────────────
* Only a credential provider, through `CredentialRequest.request`, which the
* framework fills from the run the call belongs to (`bindRequest`). The model
* never sees it, no stage writes it to scope, and a tool reaches what it
* vends only as `ctx.credential`.
*
* ── One request, one run ─────────────────────────────────────────────────────
* The framework decides WHICH handle a call carries — the agent's own run, or
* the run a composition opened for its members (`core/memberRun.ts`). A call
* with no run that carries a request gets none: a request credential then
* fails closed (`requestCredentials`), and is never borrowed from another run.
*
* Pattern: sealed value object (module-private WeakMap). Role: identity, run input.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.bindRequest = exports.requestSecretOf = exports.assertRequestContext = exports.isRequestContext = exports.requestContext = void 0;
/** The sealed values, keyed by handle. Module-private: nothing else can read them. */
const sealed = new WeakMap();
const PLACEHOLDER = '[request context]';
/**
* Seal an incoming request's secrets for one run. Pass the result as
* `run(input, { request })` (or `resume(…, { request })`); a credential
* provider built by `requestCredentials` reads it there.
*
* @throws TypeError when `secrets` is not a plain object of non-empty strings.
*/
function requestContext(options) {
const secrets = options?.secrets;
if (secrets === null || typeof secrets !== 'object' || Array.isArray(secrets)) {
throw new TypeError('requestContext: `secrets` must be an object of name → string.');
}
const values = new Map();
for (const name of Object.keys(secrets)) {
const value = secrets[name];
if (name === '')
throw new TypeError('requestContext: a secret name must not be empty.');
// The NAME is safe to say; the value never is.
if (typeof value !== 'string' || value === '') {
throw new TypeError(`requestContext: secret '${name}' must be a non-empty string — an absent value is ` +
'left out, never passed empty.');
}
values.set(name, value);
}
const handle = Object.create(handleProto);
Object.freeze(handle);
sealed.set(handle, values);
return handle;
}
exports.requestContext = requestContext;
/**
* The handle's prototype — every reader that might print or copy it gets the
* placeholder. No own properties, so `Object.keys`, a spread or
* `structuredClone` yields `{}`.
*/
const handleProto = Object.freeze({
toJSON: () => PLACEHOLDER,
toString: () => PLACEHOLDER,
[Symbol.toStringTag]: 'RequestContext',
[Symbol.for('nodejs.util.inspect.custom')]: () => PLACEHOLDER,
});
/** Whether `value` is a handle {@link requestContext} built. */
function isRequestContext(value) {
return typeof value === 'object' && value !== null && sealed.has(value);
}
exports.isRequestContext = isRequestContext;
/**
* Refuse a `request` that {@link requestContext} did not build — at the door
* the caller used, before anything moves. A plain object would carry its
* values wherever the options travel.
*
* @throws TypeError naming the door.
*/
function assertRequestContext(value, door) {
if (value === undefined || isRequestContext(value))
return;
throw new TypeError(`${door}: \`request\` must be built by requestContext({ secrets }) — a plain object is ` +
'refused, because its values would travel wherever these options are logged or copied.');
}
exports.assertRequestContext = assertRequestContext;
/**
* The secret a sealed request holds under `name`, or `undefined`.
*
* @internal Never re-exported on a door. The only reader is a credential
* provider in this folder, which turns it into a `Credential` for one call.
*/
function requestSecretOf(request, name) {
if (typeof request !== 'object' || request === null)
return undefined;
return sealed.get(request)?.get(name);
}
exports.requestSecretOf = requestSecretOf;
/**
* The provider a call of ONE run is handed: a `getCredential` for `service` —
* the service the calling tool DECLARES in its `needs` — carries that run's
* request, and only that run's. A request the caller put on the request
* itself is always replaced (or removed), so a tool cannot present another
* run's handle through `ctx.credentials`; and a call for any OTHER service
* carries no request, so a tool cannot pull a request credential it never
* declared. `service` undefined → no call carries a request.
*
* Every other field passes through untouched; the provider's `id` is kept.
*
* @internal The framework's binding; not on a door.
*/
function bindRequest(provider, request, service) {
return {
id: provider.id,
getCredential(req) {
const { request: _presented, ...rest } = req;
void _presented;
const carries = request !== undefined && service !== undefined && req.service === service;
return provider.getCredential(carries ? { ...rest, request } : rest);
},
};
}
exports.bindRequest = bindRequest;
//# sourceMappingURL=requestContext.js.map