TypeScript SDK
The complete @veefore/sdk surface — protect, the client, typed errors and the assertion helpers.
bashnpm install @veefore/sdk
ESM and CommonJS builds with type declarations and no runtime dependencies.
Works on Node 20.10+ and in any runtime with fetch and WebCrypto.
Running veefore in your project installs this package, writes its
configuration and wraps your tools for you. See
Automatic setup.
A convenience layer, never a security boundary
Every protected action is decided server-side by the gateway on every call. Nothing in this package can grant an agent authority it does not already have, and nothing here caches a permission.
#guard(toolKey, fn, options?)
The way most agents integrate. Wrap the function that performs a tool, and every call is decided by the gateway before your function runs.
tsimport { guard } from '@veefore/sdk';
const refundCustomer = tool({
description: 'Refund a customer order',
parameters: z.object({ orderId: z.string(), amount: z.number() }),
execute: guard('refund_customer', async ({ orderId, amount }) =>
payments.refunds.create({ orderId, amount }),
),
});
| Decision | What guard() does |
|---|---|
Permitted, CLIENT tool |
Runs your function and returns its result |
Permitted, brokered HTTP tool |
Returns the gateway's result without running your function |
| Refused | Throws VeeforeBlockedError |
| Needs a human | Throws VeeforeApprovalRequiredError, or waits with waitForApproval |
The tool's first argument is sent as the policy parameters. The wrapped
function always returns a Promise, so wrap async functions.
capabilitystringoptionalDefaults to
tool.<toolKey>, whichveefore initregisters.waitForApprovalboolean or { timeoutMs }optionalWait for a human instead of throwing. Suits background jobs; in a chat turn, throwing lets the model tell the user.
describe(...args) => Partial<ActionRequest>optionalAdds policy inputs derived from the call, such as
amount,currency,destinationorpurpose.clientVeeforeClientoptionalUse this client instead of the shared one built from the environment.
tsexecute: guard('refund_customer', refund, {
describe: ({ amount }) => ({ amount, currency: 'USD', purpose: 'Customer refund' }),
}),
Configuration comes from the VEEFORE_* environment variables. If they are not
set, guard() reads the block veefore init writes to .env.local; values
already in the environment always win.
#protect(agent, options)
For code that calls Veefore directly rather than through a tool function.
tsimport { protect } from '@veefore/sdk';
const agent = protect(myAgent, { name: 'finance-agent' });
Returns a Proxy over your object. Own properties and methods pass through
untouched; veefore, act, evaluate, simulate and handshake are layered
on top, and only where your object does not already define them.
namestringoptionalDisplay metadata. Never used for authorization — identity is
agentId.gatewayUrlstringrequiredFrom
VEEFORE_GATEWAY_URLif not passed.agentIdstringrequiredFrom
VEEFORE_AGENT_ID.privateKeystringrequiredBase64 PKCS#8 Ed25519. From
VEEFORE_AGENT_PRIVATE_KEY. Never logged, never sent anywhere, never written to disk by the SDK.keyIdstringrequiredFrom
VEEFORE_AGENT_KEY_ID. Identifies which key, which is what makes rotation work.audiencestringoptionalBound into assertions. Must match the gateway's published audience exactly. Defaults to
gatewayUrl.declaredToolsstring[]optionalTools you intend to use, reported at handshake so anything unauthorized is a startup warning rather than a mid-task failure.
eagerHandshakebooleanoptionalOpens the session at construction so misconfiguration surfaces immediately. Defaults to
falseto keepprotect()synchronous.requestTimeoutMsnumberoptionalPer-request timeout.
maxRetriesnumberoptionalRetries for transport errors only. Decisions are never retried.
fetchtypeof fetchoptionalInject your own, for instrumentation or a custom agent pool.
onLog(entry: SdkLogEntry) => voidoptionalStructured diagnostics. Never receives secrets. The SDK does not write to stdout on its own.
Missing required configuration throws VeeforeConfigurationError at
construction, naming exactly what is absent.
#createClient(options) and configFromEnv(env?)
tsimport { createClient, configFromEnv } from '@veefore/sdk';
const client = createClient({ name: 'worker' }); // env + overrides
const partial = configFromEnv(); // just read the env
Use createClient when you have no agent object to wrap — a worker, a cron job,
a service.
#VeeforeClient
#handshake()
tsconst info = await agent.handshake();
Returns HandshakeResult: agentId, mode, environment, organization,
authorizedTools, capabilities, passportVersion, expiresAt.
Advisory. Useful for failing fast and for logging; not a permission grant, and not trusted on the next call.
#authenticate() and info
tsawait agent.veefore.authenticate(); // open a session without a full handshake
agent.veefore.info; // last HandshakeResult, or null
#act(request)
Performs the action. Throws when refused.
#evaluate(request)
Returns the ActionDecision as data. Throws only on transport and
authentication failures, so a BLOCK comes back as a value.
#simulate(request)
evaluate with dryRun: true. Evaluates without executing.
#actWithApproval(request, { timeoutMs? })
Evaluates; if an approval is required, waits for it honouring pollAfterMs,
then resubmits the identical request with the approvalId. The gateway
re-validates everything.
#approvalStatus(approvalId) and waitForApproval(approvalId, options)
tsconst status = await agent.veefore.approvalStatus('apr_01ABC');
await agent.veefore.waitForApproval('apr_01ABC', {
timeoutMs: 15 * 60 * 1000,
pollIntervalMs: 5000,
});
#mcpCall(request)
tsawait agent.veefore.mcpCall({
server: 'internal-tools',
tool: 'search_tickets',
arguments: { query: 'refund failed' },
});
#a2aInvoke(request)
tsawait agent.veefore.a2aInvoke({
connection: 'a2a_partner_logistics',
capability: 'shipment.query',
action: 'query',
payload: { trackingId: 'TRK-8812' },
});
#ActionRequest
tsinterface ActionRequest {
tool: string; // registered tool key — required
action?: string;
capability?: string;
parameters?: JsonObject;
resource?: { key?: string; type?: string; attributes?: JsonObject };
amount?: number | null; // minor units; a policy input only
currency?: string | null;
destination?: string | null;
purpose?: string | null; // shown on the approval card — fill this in
idempotencyKey?: string;
approvalId?: string;
dryRun?: boolean;
metadata?: JsonObject;
}
#ActionDecision
tsinterface ActionDecision {
actionId: string;
requestId: string;
traceId: string;
mode: GatewayMode;
decision: PolicyDecision; // ALLOW BLOCK REQUIRE_APPROVAL STEP_UP_AUTH LIMIT MASK
outcome: ActionOutcome;
allowed: boolean; // branch on this
result: JsonObject | null;
reason: string;
explanation: JsonObject;
approval: { required: boolean; approvalId: string | null; status: string | null;
expiresAt: string | null; pollAfterMs: number | null } | null;
risk: { level: string; score: number } | null;
shadow: { decision: PolicyDecision; reason: string } | null;
limits: { appliedAmount: number | null; maskedFields: string[] } | null;
latencyMs: number;
stages: StageResult[];
}
`decision` is not always the enforcement decision
In OBSERVE, decision is the effective outcome (ALLOW) and
shadow.decision is what enforcement would have done. allowed already
accounts for mode, which is why it is the field to branch on.
#Errors
| Class | Meaning | Retry |
|---|---|---|
VeeforeConfigurationError |
Required configuration missing | no — fix config |
VeeforeAuthenticationError |
Identity refused: rotated, revoked, replayed, bad audience | no |
VeeforeBlockedError |
Policy refused. Carries the decision | never |
VeeforeApprovalRequiredError |
Human needed. Carries approvalId |
not a retry — wait |
VeeforeApprovalDeniedError |
A human said no | no |
VeeforeRateLimitError |
429 |
yes, with backoff |
VeeforeConnectionError |
Transport failure or timeout | yes, with backoff |
VeeforeError |
Base class | depends |
tsimport { VeeforeBlockedError } from '@veefore/sdk';
try {
await agent.act(request);
} catch (error) {
if (error instanceof VeeforeBlockedError) {
error.decision; // the full ActionDecision
error.message; // the operator-authored reason from the policy rule
}
}
decisionToError(decision) converts a decision into the matching error class,
which is useful if you used evaluate() and later decide you want to throw.
#Assertion helpers
Exported so you can build the wire protocol yourself:
tsimport {
createAssertion,
generateNonce,
canonicalJson,
ASSERTION_TTL_SECONDS,
} from '@veefore/sdk';
canonicalJson is the canonicalisation the server uses — sorted keys, no
insignificant whitespace — so two implementations produce identical bytes and
identical signatures. See
Sign your own assertion.
#Retries, precisely
The SDK retries transport errors — connection failures, timeouts, 5xx —
with exponential backoff, up to maxRetries.
It never retries a decision. A 403 is the server's answer, and the answer will
not change. Retrying a refusal produces the same refusal and burns your rate
limit.