Concepts
Attestation
Bedrock Attest records what a model was asked and what it produced, at the moment of generation, and signs it into the firm's ledger.
@bedrockgovernance/attest is the SDK that captures a generation (the event of a model producing a piece of advice) and records it as an immutable ledger entry. Where a review job attests what document was produced and reviewed, an attestation captures exactly what the model saw and returned.
What a generation captures
A single attest() call records the complete generation bundle. Only model, systemPrompt, messages, and output are required. Everything else is optional, included when your pipeline has it.
{
"clientReference": "CLI-41269355",
"correlationId": "af3c1e90-6b2d-4e7a-9c11-0d2b8e5f4a12",
"adviser": { "name": "Jane Smith", "fcaRef": "JXS01234" },
"model": {
"provider": "anthropic",
"name": "claude-opus-4-8",
"parameters": { "temperature": 0.2, "maxTokens": 4096 }
},
"systemPrompt": "You are a suitability report assistant. Never recommend...",
"messages": [
{ "role": "user", "content": "Draft a suitability report for a balanced investor..." }
],
"promptTemplate": { "id": "suitability-v3", "version": "3.2.1", "hash": "sha256:9f2c..." },
"retrievedContext": [
{ "source": "intelliflo:factfind", "reference": "20655934", "hash": "sha256:1a4b..." }
],
"guardrails": [
{ "rule": "MAX_EQUITY_ALLOCATION", "triggered": false },
{ "rule": "VULNERABLE_CLIENT_CHECK", "triggered": true, "action": "flagged_for_review" }
],
"output": {
"content": "## Suitability Report\n\nBased on your balanced risk profile...",
"finishReason": "stop"
},
"usage": { "inputTokens": 3120, "outputTokens": 1840 }
}Fields
| Field | Type | Req | Description |
|---|---|---|---|
model | object | Yes | { provider, name, version?, parameters? }. Pin the exact name and version, not a moving alias, so drift detection stays meaningful. |
systemPrompt | string | Yes | System instructions, exactly as sent to the model. |
messages | array | Yes | The full conversation context, in order, verbatim. Canonicalised into the record hash so any later change is detectable. |
output | object | Yes | What the model produced: { content, finishReason?, toolCalls? }. Bedrock stamps outputHash so the generation can be matched to the submitted document. |
promptTemplate | object | { id, version, hash } of the template that rendered the prompt, if you use one. | |
retrievedContext | array | Grounding documents given to the model: { source, reference?, hash, content? }. The hash makes content provable even when stored by reference; large corpora can omit content. | |
tools | array | Tool or function definitions the model could call, verbatim. | |
guardrails | array | Rules evaluated during generation: { rule, triggered, action? }. Same shape as AI context. | |
usage | object | Token counts: { inputTokens, outputTokens }. | |
adviser | object | { name, fcaRef } of the responsible individual, recorded as the ledger actor under SM&CR. Optional: when omitted, the record is attributed to the firm credential that authenticated the call. | |
correlationId | string | Yes | A tool-minted id shared by every generation working toward the same piece of advice. The join key that groups them into one set; see Linking generations. |
supersedes | string | generationId of a prior generation this one revises, giving an ordered draft, revision, final lineage. Optional; without it the ledger sequence gives chronological order. | |
clientReference | string | Links the generation to the client record. Include it when your tool has the CRM's client identifier; otherwise it is resolved from the review job when the set binds. | |
documentReference | string | The document identifier once it exists. Usually unknown at generation time. |
Two ways to call it
Attest explicitly when you want to hand Bedrock the bundle yourself:
import { Bedrock } from '@bedrockgovernance/attest';
const bedrock = new Bedrock({ apiKey: process.env.BEDROCK_API_KEY });
// Mint one correlationId when the drafting session starts, reuse it
// for every attest() call and again at submission.
const correlationId = crypto.randomUUID();
const { generationId } = await bedrock.attest({
clientReference,
correlationId,
adviser: { name: 'Jane Smith', fcaRef: 'JXS01234' },
model: { provider: 'anthropic', name: 'claude-opus-4-8' },
systemPrompt,
messages,
retrievedContext,
output: { content: draft, finishReason: 'stop' },
});Or wrap your model with the AI SDK middleware so every call is notarised automatically, with no change to your generation code:
import { wrapLanguageModel } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { attestMiddleware } from '@bedrockgovernance/attest/ai-sdk';
const model = wrapLanguageModel({
model: anthropic('claude-opus-4-8'),
middleware: attestMiddleware({ bedrock, clientReference, correlationId, adviser }),
});
// Every generateText / streamText call now writes an attestation.The middleware captures the request parameters and the result and posts the attestation in the background, so it never adds latency to the response. For stacks that do not use the AI SDK, the explicit attest() call works from any runtime.
How it links into the chain
An attestation is written as a GENERATION_RECORDED ledger record through the same write path as every other event: canonicalised, hashed, chained to the previous record, signed with the firm's key, and mirrored to object-locked storage. It becomes a first-class link in the firm's ledger before the document is submitted.
- The generation tool calls
attest(), tagging the call with acorrelationIdit minted for this piece of advice. Bedrock records aGENERATION_RECORDEDevent and returns agenerationId. - The advice document is saved to the CRM or advice platform.
- When the document is submitted for review, the submission carries the same
correlationId. That binds the whole generation set to the review job and seals it. The submitted document's hash is compared to the generations'outputHashvalues; a match to the final generation confirms the model's output was used verbatim, a mismatch is recorded as a human edit after generation. - The completed chain reads
GENERATION_RECORDED(one or more) thenDOCUMENT_SUBMITTEDthen the review outcome.
Linking multiple generations to one document
A single piece of advice is rarely one model call. Each generation is a separate attest() call and a separate GENERATION_RECORDED record, and all of them need to resolve to the one document that is eventually submitted.
correlationIdis the join key. Your tool mints one id when a drafting session begins and tags everyattest()call with it, so the firm's ledger holds a set of generations rather than isolated records.supersedesorders the set, optionally. Each revision names thegenerationIdit replaces, so the records form an explicit draft, revision, final lineage. It is enrichment, not the join: leave it out and the ledger sequence still gives chronological order.
The submission is what marks the set complete. The result is that a regulator can see not just the prompt behind the final wording, but every intermediate prompt and output that shaped it, each independently hashed into the chain.
Try it
npm install @bedrockgovernance/attest