agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
130 lines (129 loc) • 6.82 kB
TypeScript
/**
* hostFilled — tool arguments the HOST fills, from the run's credential or
* identity. The model never sees them and cannot set them.
*
* ```ts
* import { hostFilled, fromCredential, fromIdentity } from 'agentfootprint';
*
* // An MCP server whose every tool takes a `token` argument:
* const tools = hostFilled(await crm.tools(), {
* token: fromCredential({ service: 'crm' }), // ctx.credential, resolved per call
* tenantId: fromIdentity('tenant'), // ctx.identity.tenant
* });
* ```
*
* ── What it does ────────────────────────────────────────────────────────────
* 1. **Removes the field from the schema the model sees** (`properties` and
* `required`). The model is never told the argument exists.
* 2. **Refuses a model-supplied value by name.** A call that sends the field
* anyway — guessed, injected by a document, copied from another tool — is
* refused with a result naming the argument, and the tool does not run.
* 3. **Fills it at the last moment**, inside `execute`, from `ctx.credential`
* (the tool's declared `needs`, which `fromCredential({ service })` sets
* when the tool has none) or `ctx.identity`. A missing value refuses the
* call by name — never an empty or default value.
*
* ── Why a wrapper around `execute` ──────────────────────────────────────────
* The filled value exists only inside the call: the arguments every dispatch
* door records (`tool_start`, history, the commit log, a consent pause's
* checkpoint) are the MODEL's arguments, which never held it. Wrapping
* `execute` makes that true at every door — the agent loop, a resumed call,
* an inner `ctx.tools` call and `mcpServe` — without each door knowing.
*
* A credential value is also SCRUBBED on the way out: every occurrence in the
* tool's result is replaced by `[host-filled]`, and a thrown error that
* carries it (an SDK that echoes its request) is replaced by one that does
* not. That is defence in depth for a server that echoes; it cannot cover a
* value the tool transforms (base64, hashing) or sends elsewhere itself.
* Identity values are not secrets and are not scrubbed.
*
* Pattern: Decorator over `Tool` (declaration + execute). Role: tools, argument law.
*/
import type { Credential } from '../identity/types.js';
import type { HostFilledArguments } from '../semantics/coverage/search/inEffect.js';
import { type Tool } from './tools.js';
/** Which identity field a {@link fromIdentity} fill reads. */
export type HostFilledIdentityField = 'principal' | 'tenant' | 'conversationId';
/** Fill from the call's resolved credential (`ctx.credential`). */
export interface HostFillFromCredential {
readonly from: 'credential';
/** The service (`needs.credential`) — required when the tool declares no `needs`. */
readonly service?: string;
/** Scopes / mode for the `needs` this fill declares (ignored when the tool has its own). */
readonly scopes?: readonly string[];
readonly mode?: 'machine' | 'user';
/** Credential → argument value. Default: a bearer's `token`, an API key's `key`. */
readonly read?: (credential: Credential) => string;
}
/** Fill from the identity the run's caller named (`ctx.identity`). */
export interface HostFillFromIdentity {
readonly from: 'identity';
readonly field: HostFilledIdentityField;
}
/** Where a host-filled argument's value comes from. */
export type HostFill = HostFillFromCredential | HostFillFromIdentity;
/**
* Fill the argument from the call's credential. With `service`, the tool's
* `needs` is declared for you (a tool that already declares one must name the
* same service, or omit `service`).
*/
export declare function fromCredential(options?: Omit<HostFillFromCredential, 'from'>): HostFillFromCredential;
/** Fill the argument from the identity the run's caller named. */
export declare function fromIdentity(field: HostFilledIdentityField): HostFillFromIdentity;
/**
* A model sent a value for an argument the host fills. The call is refused
* and the tool does not run. Names the arguments, never a value.
*/
export declare class HostFilledArgumentError extends Error {
readonly toolName: string;
readonly fields: readonly string[];
readonly code: "ERR_HOST_FILLED_ARGUMENT";
constructor(toolName: string, fields: readonly string[]);
}
/**
* A host-filled argument had no value for this call — no credential was
* resolved, the credential has no single value to read, or the run's caller
* named no such identity field. The call is refused; nothing is guessed.
*/
export declare class HostFilledValueMissingError extends Error {
readonly toolName: string;
readonly field: string;
readonly code: "ERR_HOST_FILLED_VALUE_MISSING";
constructor(toolName: string, field: string, why: string);
}
/**
* Which arguments `hostFilled` fills on `tool`, by where their value comes
* from — the names only, never a value. `undefined` for a tool no
* `hostFilled` returned.
*
* The filled values exist only inside `execute`, so the arguments every door
* records never hold them; a reader that composes ground from a call's
* arguments (`Tool.search` composition) learns the filled set here: an
* identity-filled argument is a filter in effect, a credential-filled one
* names no ground.
*
* @example
* ```ts
* hostFillsOf(hostFilled(tool, { tenantId: fromIdentity('tenant') }));
* // { identity: { tenantId: 'tenant' }, credential: [] }
* ```
*/
export declare function hostFillsOf(tool: Tool): HostFilledArguments | undefined;
/** What replaces a credential value in a result or an error. */
export declare const HOST_FILLED_PLACEHOLDER = "[host-filled]";
/**
* Declare tool arguments as filled by the host.
*
* One tool: every named argument must be a property of its `inputSchema`.
* A list (e.g. `mcpClient().tools()`): each tool takes the fills for the
* arguments IT declares and is returned unchanged when it declares none; an
* argument no tool in the list declares is refused (a typo would otherwise
* hide nothing).
*
* @throws TypeError at build — an unknown argument, a fill that is not one,
* a credential fill with no service on a tool that declares no `needs`, a
* service that disagrees with the tool's own `needs`, or an argument the
* tool also declares under `wants` / `askOrAssume`.
*/
export declare function hostFilled<T extends Tool>(tool: T, fills: Readonly<Record<string, HostFill>>): Tool;
export declare function hostFilled(tools: readonly Tool[], fills: Readonly<Record<string, HostFill>>): readonly Tool[];