aira-sdk 0.4.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,9 @@
1
- # Aira TypeScript SDK
2
-
3
- **AI governance infrastructure.**
1
+ # Aira TypeScript SDK — The authorization and audit layer for AI agents.
4
2
 
5
3
  [![npm version](https://img.shields.io/npm/v/aira-sdk.svg)](https://www.npmjs.com/package/aira-sdk)
6
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
7
5
 
8
- Policies evaluate every agent action. Humans approve high-stakes decisions. Cryptographic receipts prove it all. Rules, AI, and multi-model consensus govern what your agents can do with Ed25519 signatures, RFC 3161 timestamps, and W3C DID identity. Built for EU AI Act, SR 11-7, and GDPR compliance.
6
+ Drop Aira into your agent stack in one line. Define policies without changing code. Get cryptographic proof of every decision for your auditors, your board, or a court. Not because regulation requires it. Because your agents are acting in production right now.
9
7
 
10
8
  ```bash
11
9
  npm install aira-sdk
@@ -23,42 +21,176 @@ npm install aira-sdk @openai/agents # for OpenAI Agents
23
21
 
24
22
  ## Quick Start
25
23
 
26
- Every call to `notarize()` returns a cryptographic receipt -- Ed25519-signed, timestamped, tamper-proof.
24
+ Aira uses a **two-step flow**: `authorize()` BEFORE the action runs,
25
+ `notarize()` AFTER. This lets you block disallowed actions before they have
26
+ any real-world effect, and still produce a cryptographic receipt once the
27
+ action has completed.
27
28
 
28
29
  ```typescript
29
- import { Aira } from "aira-sdk";
30
+ import { Aira, AiraError } from "aira-sdk";
30
31
 
31
32
  const aira = new Aira({ apiKey: "aira_live_xxx" });
32
33
 
33
- const receipt = await aira.notarize({
34
- actionType: "email_sent",
35
- details: "Sent onboarding email to customer@example.com",
36
- agentId: "support-agent",
34
+ try {
35
+ // Step 1 — ask Aira whether the action is allowed.
36
+ const auth = await aira.authorize({
37
+ actionType: "wire_transfer",
38
+ details: "Send €75K to vendor X",
39
+ agentId: "payments-agent",
40
+ });
41
+
42
+ if (auth.status === "authorized") {
43
+ // Step 2a — execute the action, then notarize the outcome.
44
+ const result = await sendWire(75_000, "vendor");
45
+ await aira.notarize({
46
+ actionId: auth.action_id,
47
+ outcome: "completed",
48
+ outcomeDetails: `Sent. ref=${result.id}`,
49
+ });
50
+ } else if (auth.status === "pending_approval") {
51
+ // Step 2b — enqueue for the approver flow; do NOT execute.
52
+ await queue.enqueue(auth.action_id);
53
+ }
54
+ } catch (e) {
55
+ // Step 2c — a policy denied the action. Nothing to execute, nothing to
56
+ // notarize. POLICY_DENIED is thrown, never returned as a status.
57
+ if (e instanceof AiraError && e.code === "POLICY_DENIED") {
58
+ console.log(`Blocked: ${e.message}`);
59
+ } else {
60
+ throw e;
61
+ }
62
+ }
63
+ ```
64
+
65
+ The returned `ActionReceipt` carries the Ed25519 signature and RFC 3161
66
+ timestamp token. If you pass `outcome: "failed"`, the backend still writes
67
+ an audit entry but leaves `signature` / `receipt_id` null.
68
+
69
+ ### Reproducibility metadata (replay context)
70
+
71
+ Pass any of the following optional fields to `authorize()` and they're committed in the signed receipt payload (v1.3) and surfaced via `getReplayContext()` so an external replay tool can confirm it has the same inputs as the original run:
72
+
73
+ ```typescript
74
+ const auth = await aira.authorize({
75
+ actionType: "tool_call",
76
+ details: "Calling search() with structured input",
77
+ agentId: "research-agent",
37
78
  modelId: "claude-sonnet-4-6",
38
- instructionHash: "sha256:a1b2c3...",
79
+ // Optional reproducibility metadata
80
+ systemPromptHash: "sha256:a1b2c3...",
81
+ toolInputsHash: "sha256:d4e5f6...",
82
+ modelParams: { temperature: 0.0, top_p: 1.0, seed: 42 },
83
+ executionEnv: {
84
+ sdk_version: "2.0.1",
85
+ framework: "langchain",
86
+ framework_version: "0.3.0",
87
+ },
88
+ });
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Compliance bundles
94
+
95
+ Seal a regulator-ready evidence bundle for a date range. Every receipt in the period is Merkle-rooted, signed, and the export is JWKS-verifiable offline.
96
+
97
+ ```typescript
98
+ // Build a Q1 2026 EU AI Act Article 12 evidence packet
99
+ const bundle = await aira.createComplianceBundle({
100
+ framework: "eu_ai_act_art12", // or iso_42001, aiuc_1, soc_2_cc7, raw
101
+ periodStart: "2026-01-01T00:00:00Z",
102
+ periodEnd: "2026-04-01T00:00:00Z",
103
+ title: "Q1 2026 evidence packet",
104
+ agentFilter: ["payments-agent", "support-agent"],
105
+ });
106
+ console.log(bundle.merkle_root, bundle.receipt_count);
107
+
108
+ // Download the self-contained JSON for an auditor
109
+ const exported = await aira.exportComplianceBundle(bundle.id as string);
110
+ // `exported` includes every receipt, the JWKS URL, and a verification recipe.
111
+ ```
112
+
113
+ ## Drift detection
114
+
115
+ Per-agent behavioral baselines + KL divergence scoring + alerts when an agent's behavior shifts away from its expected pattern.
116
+
117
+ ```typescript
118
+ // Compute a baseline from the last 7 days of action history
119
+ const end = new Date();
120
+ const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000);
121
+
122
+ await aira.computeDriftBaseline({
123
+ agentId: "payments-agent",
124
+ windowStart: start.toISOString(),
125
+ windowEnd: end.toISOString(),
126
+ });
127
+
128
+ // Or seed a baseline from a config dict (cold start)
129
+ await aira.seedSyntheticBaseline({
130
+ agentId: "payments-agent",
131
+ expectedDistribution: { wire_transfer: 0.05, email_sent: 0.40, api_call: 0.55 },
132
+ expectedActionsPerDay: 200,
39
133
  });
40
134
 
41
- console.log(receipt.payload_hash); // sha256:e5f6a7b8...
42
- console.log(receipt.signature); // ed25519:base64url...
43
- console.log(receipt.action_id); // uuid — publicly verifiable
135
+ // Read-only status check for dashboards
136
+ const status = await aira.getDriftStatus("payments-agent", 24);
137
+ console.log(status.kl_divergence, status.severity);
138
+
139
+ // Run a check that records an alert if the threshold is exceeded
140
+ const alert = await aira.runDriftCheck("payments-agent");
141
+ if (alert) console.log(`Drift detected: ${alert.severity}`);
142
+ ```
143
+
144
+ ## Merkle settlement
145
+
146
+ Periodic Merkle anchoring of action receipts. Every receipt eventually gets sealed into exactly one settlement; the settlement's Merkle root is the cryptographic commitment that the batch existed at a specific moment in time.
147
+
148
+ ```typescript
149
+ // Admin: seal all unsettled receipts
150
+ const settlement = await aira.createSettlement();
151
+ if (settlement) {
152
+ console.log(settlement.merkle_root, settlement.receipt_count);
153
+ }
154
+
155
+ // An auditor wants to prove a single receipt was in a settlement
156
+ const proof = await aira.getSettlementInclusionProof("rct-abc-123");
157
+ // proof has { merkle_root, leaf_hash, index, leaf_count, siblings }
158
+ // A regulator can verify it offline with a 10-line pure-function walker.
44
159
  ```
45
160
 
46
161
  ---
47
162
 
48
163
  ## Core SDK Methods
49
164
 
50
- All 52 methods on `Aira`. Every write operation produces a cryptographic receipt.
165
+ Every write operation produces a cryptographic receipt.
51
166
 
52
167
  | Category | Method | Description |
53
168
  |---|---|---|
54
- | **Actions** | `notarize()` | Notarize an action -- returns Ed25519-signed receipt (supports `requireApproval`) |
169
+ | **Actions** | `authorize()` | Step 1 — authorize an action BEFORE it runs (throws POLICY_DENIED). Accepts optional replay context fields (`systemPromptHash`, `toolInputsHash`, `modelParams`, `executionEnv`). |
170
+ | | `notarize()` | Step 2 — notarize the outcome, returns Ed25519-signed receipt |
55
171
  | | `getAction()` | Retrieve action details + receipt |
56
172
  | | `listActions()` | List actions with filters (type, agent, status) |
57
- | | `authorizeAction()` | Human co-signature on high-stakes action |
173
+ | | `cosign()` | Human co-signature on a high-stakes action |
58
174
  | | `setLegalHold()` | Prevent deletion -- litigation hold |
59
175
  | | `releaseLegalHold()` | Release litigation hold |
60
176
  | | `getActionChain()` | Chain of custody for an action |
61
- | | `verifyAction()` | Public verification -- no auth required |
177
+ | | `getReplayContext()` | All reproducibility metadata for an action (system prompt hash, tool inputs hash, model params, execution env) |
178
+ | | `verifyAction()` | Public verification -- no auth required. Returns full evidence (signature, public key, signed payload, RFC 3161 token) plus the second-party `policy_evaluator_attestation` for multi-party signing. |
179
+ | **Compliance** | `createComplianceBundle()` | Seal a regulator-ready evidence bundle for a date range. Frameworks: `eu_ai_act_art12`, `iso_42001`, `aiuc_1`, `soc_2_cc7`, `raw`. Merkle-rooted, signed, JWKS-verifiable offline. |
180
+ | | `listComplianceBundles()` | List bundles for the org |
181
+ | | `getComplianceBundle()` | Get bundle metadata |
182
+ | | `exportComplianceBundle()` | Download the self-contained JSON for offline verification |
183
+ | | `getBundleInclusionProof()` | Merkle inclusion proof for one receipt within a bundle |
184
+ | **Drift** | `getDriftStatus()` | Read-only KL divergence + volume ratio against the active baseline |
185
+ | | `computeDriftBaseline()` | Build a baseline from a window of production action history |
186
+ | | `seedSyntheticBaseline()` | Seed a baseline from a config dict (cold start) |
187
+ | | `runDriftCheck()` | Score the current window and persist a `DriftAlert` if it exceeds the threshold |
188
+ | | `listDriftAlerts()` | List drift alerts for an agent |
189
+ | | `acknowledgeDriftAlert()` | Acknowledge an alert |
190
+ | **Settlement** | `createSettlement()` | Seal every unsettled receipt into a Merkle-rooted, signed batch (admin-only) |
191
+ | | `listSettlements()` | List settlements |
192
+ | | `getSettlement()` | Get settlement metadata |
193
+ | | `getSettlementInclusionProof()` | Get a receipt's Merkle inclusion proof from its settlement |
62
194
  | **Agents** | `registerAgent()` | Register verifiable agent identity |
63
195
  | | `getAgent()` | Retrieve agent profile |
64
196
  | | `listAgents()` | List registered agents |
@@ -167,37 +299,35 @@ console.log(rep.tier); // "Verified"
167
299
 
168
300
  ### Endpoint Verification
169
301
 
170
- Control which external APIs your agents can call. When `endpointUrl` is passed to `notarize()`, Aira checks it against your org's whitelist. Unrecognized endpoints are blocked in strict mode.
171
-
172
- #### Notarize with endpointUrl
173
-
174
- ```typescript
175
- const receipt = await aira.notarize({
176
- actionType: "api_call",
177
- details: "Charged customer $49.99 for subscription renewal",
178
- agentId: "billing-agent",
179
- modelId: "claude-sonnet-4-6",
180
- endpointUrl: "https://api.stripe.com/v1/charges",
181
- });
182
- ```
183
-
184
- #### Handle ENDPOINT_NOT_WHITELISTED
302
+ Control which external APIs your agents can call. When `endpointUrl` is
303
+ passed to `authorize()`, Aira checks it against your org's whitelist
304
+ before returning. Unrecognized endpoints throw `ENDPOINT_NOT_WHITELISTED`
305
+ in strict mode — the action is never authorized and you never need to
306
+ call `notarize()`.
185
307
 
186
308
  ```typescript
187
309
  import { Aira, AiraError } from "aira-sdk";
188
310
 
189
311
  try {
190
- const receipt = await aira.notarize({
312
+ const auth = await aira.authorize({
191
313
  actionType: "api_call",
192
- details: "Send SMS via new provider",
193
- agentId: "notifications-agent",
194
- endpointUrl: "https://api.newprovider.com/v1/sms",
314
+ details: "Charged customer $49.99 for subscription renewal",
315
+ agentId: "billing-agent",
316
+ modelId: "claude-sonnet-4-6",
317
+ endpointUrl: "https://api.stripe.com/v1/charges",
195
318
  });
319
+
320
+ if (auth.status === "authorized") {
321
+ const result = await stripe.charges.create({ amount: 4999, /* ... */ });
322
+ await aira.notarize({
323
+ actionId: auth.action_id,
324
+ outcome: "completed",
325
+ outcomeDetails: `Charged ${result.id}`,
326
+ });
327
+ }
196
328
  } catch (e) {
197
329
  if (e instanceof AiraError && e.code === "ENDPOINT_NOT_WHITELISTED") {
198
330
  console.log(`Blocked: ${e.message}`);
199
- console.log(`Approval request: ${e.details.approval_id}`);
200
- console.log(`Suggested pattern: ${e.details.url_pattern_suggested}`);
201
331
  } else {
202
332
  throw e;
203
333
  }
@@ -227,68 +357,90 @@ const handler = new AiraCallbackHandler(aira, "research-agent", {
227
357
 
228
358
  ## Session
229
359
 
230
- Pre-fill defaults for a block of related actions. Every `notarize()` call within the session inherits the agent identity, producing receipts that share a common provenance chain.
360
+ Pre-fill defaults for a block of related actions. Every `authorize()` call
361
+ within the session inherits the agent identity, producing receipts that
362
+ share a common provenance chain.
231
363
 
232
364
  ```typescript
233
365
  const sess = aira.session("onboarding-agent", { modelId: "claude-sonnet-4-6" });
234
366
 
235
- await sess.notarize({ actionType: "identity_verified", details: "Verified customer ID #4521" });
236
- await sess.notarize({ actionType: "account_created", details: "Created account for customer #4521" });
237
- await sess.notarize({ actionType: "welcome_sent", details: "Sent welcome email to customer #4521" });
367
+ async function notarize(actionType: string, details: string) {
368
+ const auth = await sess.authorize({ actionType, details });
369
+ if (auth.status === "authorized") {
370
+ await sess.notarize({ actionId: auth.action_id, outcome: "completed" });
371
+ }
372
+ }
373
+
374
+ await notarize("identity_verified", "Verified customer ID #4521");
375
+ await notarize("account_created", "Created account for customer #4521");
376
+ await notarize("welcome_sent", "Sent welcome email to customer #4521");
238
377
  ```
239
378
 
240
379
  ---
241
380
 
242
381
  ## Offline Mode
243
382
 
244
- Queue notarizations locally when connectivity is unavailable. Cryptographic receipts are generated server-side when you sync -- nothing is lost.
383
+ Queue `authorize()` calls locally when connectivity is unavailable. On
384
+ `sync()`, the backend runs the real authorizations and returns their
385
+ results. The agent can then call `notarize()` per action to close the loop.
245
386
 
246
387
  ```typescript
247
388
  const aira = new Aira({ apiKey: "aira_live_xxx", offline: true });
248
389
 
249
390
  // These queue locally — no network calls
250
- await aira.notarize({ actionType: "scan_completed", details: "Scanned document batch #77" });
251
- await aira.notarize({ actionType: "classification_done", details: "Classified 142 documents" });
391
+ await aira.authorize({ actionType: "scan_completed", details: "Scanned document batch #77" });
392
+ await aira.authorize({ actionType: "classification_done", details: "Classified 142 documents" });
252
393
 
253
394
  console.log(aira.pendingCount); // 2
254
395
 
255
- // Flush to API when back online — receipts are generated for each action
396
+ // Flush to the API when back online — returns the Authorization for each
397
+ // queued request. Use the action_ids to notarize outcomes afterwards.
256
398
  const results = await aira.sync();
257
399
  ```
258
400
 
401
+ Offline mode is intended for actions the agent will execute regardless of
402
+ Aira's decision (sensor reads, local scans). For actions whose execution
403
+ depends on the authorization result, run the SDK online.
404
+
259
405
  ---
260
406
 
261
407
  ## Human Approval
262
408
 
263
- Hold high-stakes actions for human review before the cryptographic receipt is issued. Approvers receive an email with Approve/Deny buttons.
409
+ Hold high-stakes actions for human review before they execute. Approvers
410
+ receive an email with Approve/Deny buttons. If the action is held, the SDK
411
+ returns `status: "pending_approval"` from `authorize()` and the agent must
412
+ enqueue the `action_id` instead of executing.
264
413
 
265
414
  ```typescript
266
- const receipt = await aira.notarize({
415
+ const auth = await aira.authorize({
267
416
  actionType: "loan_decision",
268
- details: "Approved €15,000 loan for Maria Schmidt",
417
+ details: "Approve €15,000 loan for Maria Schmidt",
269
418
  agentId: "lending-agent",
270
419
  requireApproval: true,
271
420
  approvers: ["compliance@acme.com", "risk@acme.com"],
272
421
  });
273
- console.log(receipt.status); // "pending_approval"
274
- console.log(receipt.receipt_id); // undefined — no receipt until approved
275
422
 
276
- // Falls back to org default approvers (Settings Approvers)
277
- const receipt2 = await aira.notarize({
278
- actionType: "wire_transfer",
279
- details: "Transfer $50,000 to vendor account",
280
- agentId: "payments-agent",
281
- requireApproval: true,
282
- });
423
+ if (auth.status === "pending_approval") {
424
+ // Do NOT execute the action. Store action_id and wait for the
425
+ // action.approved webhook, then execute + notarize.
426
+ await queue.enqueue(auth.action_id);
427
+ }
283
428
  ```
284
429
 
285
- The approver clicks "Approve" in the email → receipt is minted with Ed25519 signature + RFC 3161 timestamp → `action.approved` webhook fires.
430
+ The approver clicks "Approve" in the email → the action transitions to
431
+ `authorized` → `action.approved` webhook fires → your handler executes the
432
+ action and calls `aira.notarize({ actionId, outcome: "completed" })`.
286
433
 
287
- Configure default approvers in the [dashboard](https://app.airaproof.com/dashboard/settings/approvers) or via the `/approvers` API.
434
+ Configure default approvers in the [dashboard](https://app.airaproof.com/dashboard/settings/approvers).
288
435
 
289
436
  ### Automatic Policy Evaluation
290
437
 
291
- Org admins configure policies in the dashboard — your code doesn't change. Every `notarize()` call is automatically evaluated against active policies before the receipt is issued.
438
+ Org admins configure policies in the dashboard — your code doesn't change.
439
+ Every `authorize()` call is automatically evaluated against active policies
440
+ before returning. If a policy denies the action, the SDK throws
441
+ `AiraError` with code `POLICY_DENIED` and the action is never persisted as
442
+ authorized. If a policy forces human review, `status` is
443
+ `pending_approval`. Otherwise `status` is `authorized`.
292
444
 
293
445
  Three evaluation modes:
294
446
 
@@ -297,21 +449,15 @@ Three evaluation modes:
297
449
  - **Consensus**: Multiple LLMs evaluate independently — disagreement triggers human review (3-10s)
298
450
 
299
451
  ```typescript
300
- // Your code stays the same — policies evaluate automatically
301
- const receipt = await aira.notarize({
302
- actionType: "wire_transfer",
303
- details: "Transfer $50,000 to vendor account",
304
- agentId: "billing-agent",
305
- });
306
-
307
- // If a policy triggers "require_approval":
308
- console.log(receipt.status); // "pending_approval"
309
- console.log(receipt.policy_evaluation); // { policy_name: "Wire transfers need approval", decision: "require_approval", ... }
310
-
311
- // If a policy triggers "deny":
312
452
  import { AiraError } from "aira-sdk";
453
+
313
454
  try {
314
- await aira.notarize({ actionType: "data_deletion", details: "Delete customer records" });
455
+ const auth = await aira.authorize({
456
+ actionType: "data_deletion",
457
+ details: "Delete customer records",
458
+ agentId: "support-agent",
459
+ });
460
+ // ... execute + notarize ...
315
461
  } catch (e) {
316
462
  if (e instanceof AiraError && e.code === "POLICY_DENIED") {
317
463
  console.log(e.message); // "Action denied by policy 'Block deletions': ..."
@@ -319,87 +465,129 @@ try {
319
465
  }
320
466
  ```
321
467
 
322
- Every policy evaluation produces a cryptographic receipt — proof the policy was checked. The SDK `requireApproval: true` override still works and skips policy evaluation entirely.
323
-
324
468
  Configure policies at [Settings → Policies](https://app.airaproof.com/dashboard/policies).
325
469
 
326
470
  ---
327
471
 
328
472
  ## Framework Integrations
329
473
 
330
- Drop Aira into your existing agent framework with one import:
474
+ Drop Aira into your existing agent framework with one import. Every integration is honestly labeled as one of three kinds:
331
475
 
332
- | Framework | Import | Integration |
333
- |---|---|---|
334
- | **LangChain.js** | `import { AiraCallbackHandler } from "aira-sdk/extras/langchain"` | Callback handler |
335
- | **Vercel AI** | `import { AiraVercelMiddleware } from "aira-sdk/extras/vercel-ai"` | Middleware |
336
- | **OpenAI Agents** | `import { AiraGuardrail } from "aira-sdk/extras/openai-agents"` | Guardrail |
337
- | **MCP** | `import { createServer } from "aira-sdk/extras/mcp"` | MCP Server |
338
- | **Webhooks** | `import { verifySignature } from "aira-sdk/extras/webhooks"` | Verification |
476
+ - **gate** intercepts before execution and can deny. The action is authorized through Aira's policy engine *before* the framework runs the underlying call. Denied actions never run.
477
+ - **audit** — runs after execution because the host framework does not expose a pre-execution hook that can abort. Aira still records a signed receipt; it just cannot prevent the action.
478
+ - **adapter** exposes Aira's own API as a tool the host framework can call. Neither a gate nor an audit hook over other tools.
479
+
480
+ We ship fewer integrations than some competitors and label every one of them honestly. The integration matrix is generated from `INTEGRATIONS` in `aira-sdk/extras` the docs cannot drift from the code.
481
+
482
+ | Integration | Import | Type | Pre-execution gate? | Surface | Notes |
483
+ |---|---|---|---|---|---|
484
+ | **LangChain.js** | `aira-sdk/extras/langchain` | gate | Yes (tools); No (chains/LLMs) | `AiraCallbackHandler` | `handleToolStart` calls `authorize()` and throws on `POLICY_DENIED` so the tool never runs. Chain/LLM hooks are post-hoc because LangChain has no pre-execution chain hook that can abort. |
485
+ | **Vercel AI SDK** | `aira-sdk/extras/vercel-ai` | gate | Yes (`wrapTool`); No (`onFinish`) | `AiraVercelMiddleware` | `wrapTool()` wraps the tool's `execute` so `authorize()` runs before the tool body. `onStepFinish` / `onFinish` callbacks are explicitly labeled audit-only. |
486
+ | **OpenAI Agents** | `aira-sdk/extras/openai-agents` | gate | Yes | `AiraGuardrail.wrapTool()` | Wraps each tool function: `authorize()` runs before the tool body. Denied calls throw; failed calls notarize with `outcome="failed"`. |
487
+ | **MCP** | `aira-sdk/extras/mcp` | adapter | N/A | `createServer()` | MCP is bidirectional: the agent CHOOSES to call `authorize_action` / `notarize_action`. Not a wrapper over other MCP tools — it's a protocol adapter. |
488
+ | **Webhooks** | `aira-sdk/extras/webhooks` | adapter | N/A | `verifySignature()` | Standalone HMAC-SHA256 webhook signature verifier. Not an agent integration. |
339
489
 
340
490
  ### LangChain.js
341
491
 
342
- `AiraCallbackHandler` notarizes every tool call, chain completion, and LLM invocation with a cryptographic receipt. No changes to your chain logic.
492
+ `AiraCallbackHandler` runs the two-step flow on every tool, chain, and LLM
493
+ event. The `Start` callbacks call `authorize()` — if a policy denies the
494
+ action or flags it for human review, LangChain aborts the step. The `End`
495
+ and `Error` callbacks call `notarize()` with the appropriate outcome. This
496
+ is a **real authorization gate**, not just post-hoc audit logging.
343
497
 
344
498
  ```typescript
345
499
  import { Aira } from "aira-sdk";
346
500
  import { AiraCallbackHandler } from "aira-sdk/extras/langchain";
347
501
 
348
502
  const aira = new Aira({ apiKey: "aira_live_xxx" });
349
- const handler = new AiraCallbackHandler({ client: aira, agentId: "research-agent", modelId: "gpt-5.2" });
503
+ const handler = new AiraCallbackHandler(aira, "research-agent", {
504
+ modelId: "gpt-5.2",
505
+ strict: false, // fail-open on network errors; set true to fail-closed
506
+ });
350
507
 
351
- // Every tool call and chain completion gets a signed receipt
352
- const result = await chain.invoke({ input: "Analyze Q1 revenue" }, { callbacks: [handler] });
508
+ const result = await chain.invoke(
509
+ { input: "Analyze Q1 revenue" },
510
+ { callbacks: [handler.asCallbacks()] },
511
+ );
353
512
  ```
354
513
 
355
514
  ### Vercel AI
356
515
 
357
- `AiraVercelMiddleware` wraps your Vercel AI `streamText` / `generateText` calls so every model invocation is notarized with a tamper-proof receipt.
516
+ `AiraVercelMiddleware` exposes two integration points:
517
+
518
+ - **`wrapTool()`** — the real authorization gate. Calls `authorize()`
519
+ before the tool runs, notarizes the outcome afterwards. Use this for any
520
+ tool that touches the outside world.
521
+ - **`onStepFinish` / `onFinish`** — post-hoc audit callbacks. These fire
522
+ after the step has run and cannot gate execution. Useful for logging
523
+ generation metadata.
358
524
 
359
525
  ```typescript
360
526
  import { Aira } from "aira-sdk";
361
527
  import { AiraVercelMiddleware } from "aira-sdk/extras/vercel-ai";
528
+ import { tool } from "ai";
529
+ import { z } from "zod";
362
530
 
363
531
  const aira = new Aira({ apiKey: "aira_live_xxx" });
364
- const middleware = new AiraVercelMiddleware({ client: aira, agentId: "assistant-agent" });
532
+ const middleware = new AiraVercelMiddleware(aira, "assistant-agent");
533
+
534
+ const webSearch = tool({
535
+ description: "Search the web",
536
+ parameters: z.object({ query: z.string() }),
537
+ execute: middleware.wrapTool(async ({ query }) => {
538
+ return await search(query);
539
+ }, "web_search"),
540
+ });
365
541
 
366
- // Wrap your Vercel AI calls — receipts at invocation and completion
367
- const result = await middleware.wrapGenerateText({
542
+ const result = await generateText({
368
543
  model: openai("gpt-5.2"),
369
- prompt: "Summarize the contract terms",
544
+ prompt: "Find today's EU AI Act news",
545
+ tools: { webSearch },
546
+ ...middleware.asCallbacks(),
370
547
  });
371
548
  ```
372
549
 
373
550
  ### OpenAI Agents SDK
374
551
 
375
- `AiraGuardrail` wraps any tool function to automatically notarize both invocation and result with cryptographic proof.
552
+ `AiraGuardrail.wrapTool()` gates every tool invocation through Aira's
553
+ two-step flow: `authorize()` runs first and can block the tool on
554
+ `POLICY_DENIED` or `pending_approval`, then `notarize()` closes the loop
555
+ after the tool returns. Only tool names and arg keys are sent — raw user
556
+ input stays in your process.
376
557
 
377
558
  ```typescript
378
559
  import { Aira } from "aira-sdk";
379
560
  import { AiraGuardrail } from "aira-sdk/extras/openai-agents";
380
561
 
381
562
  const aira = new Aira({ apiKey: "aira_live_xxx" });
382
- const guardrail = new AiraGuardrail({ client: aira, agentId: "assistant-agent" });
563
+ const guardrail = new AiraGuardrail(aira, "assistant-agent");
383
564
 
384
- // Wrap tools every call and result gets a signed receipt
385
- const search = guardrail.wrapTool(searchTool, { toolName: "web_search" });
386
- const execute = guardrail.wrapTool(codeExecutor, { toolName: "code_exec" });
565
+ const search = guardrail.wrapTool(searchTool, "web_search");
566
+ const execute = guardrail.wrapTool(codeExecutor, "code_exec");
387
567
  ```
388
568
 
389
569
  ---
390
570
 
391
571
  ## MCP Server
392
572
 
393
- Expose Aira as an MCP tool server. Any MCP-compatible AI agent can notarize actions and verify receipts without SDK integration.
573
+ Expose Aira as an MCP tool server. Any MCP-compatible AI agent can run the
574
+ two-step flow and verify receipts without a direct SDK dependency.
394
575
 
395
576
  ```typescript
577
+ import { Aira } from "aira-sdk";
396
578
  import { createServer } from "aira-sdk/extras/mcp";
397
579
 
398
- const server = createServer({ apiKey: "aira_live_xxx" });
399
- server.listen(); // stdio transport
580
+ const aira = new Aira({ apiKey: "aira_live_xxx" });
581
+ const { listTools, callTool } = createServer(aira);
582
+ // Wire into @modelcontextprotocol/sdk's Server.
400
583
  ```
401
584
 
402
- The server exposes three tools: `notarize_action`, `verify_action`, and `get_receipt` -- each producing cryptographically signed results.
585
+ The server exposes the two-step flow as explicit tools:
586
+ `authorize_action`, `notarize_action`, `get_action`, `verify_action`,
587
+ `get_receipt`, plus trust-layer helpers. An MCP client is expected to call
588
+ `authorize_action` before performing a side effect and `notarize_action`
589
+ after — the MCP protocol has no hidden hook point, so the authorization
590
+ gate only exists if the agent cooperates with the contract.
403
591
 
404
592
  Add to your MCP client config:
405
593
 
@@ -449,17 +637,23 @@ Supported event types: `action.notarized`, `action.authorized`, `agent.registere
449
637
  import { Aira, AiraError } from "aira-sdk";
450
638
 
451
639
  try {
452
- await aira.notarize({ actionType: "test", details: "test" });
640
+ const auth = await aira.authorize({ actionType: "test", details: "test" });
641
+ // ...
453
642
  } catch (e) {
454
643
  if (e instanceof AiraError) {
455
- console.log(e.status); // 429
456
- console.log(e.code); // "PLAN_LIMIT_EXCEEDED"
457
- console.log(e.message); // "Monthly operation limit reached"
644
+ console.log(e.status); // e.g. 403
645
+ console.log(e.code); // e.g. "POLICY_DENIED"
646
+ console.log(e.message);
458
647
  }
459
648
  }
460
649
  ```
461
650
 
462
- All framework integrations (LangChain.js, Vercel AI, OpenAI Agents) are non-blocking by default -- notarization failures are logged, never raised. Your agent keeps running.
651
+ Framework integrations (LangChain.js, Vercel AI, OpenAI Agents) **fail open
652
+ by default** on transient errors (network, 5xx) — a warning is logged and
653
+ the tool still runs. `POLICY_DENIED` and `pending_approval` always
654
+ propagate as thrown errors so disallowed actions are never executed. Pass
655
+ `strict: true` to the integration constructor to fail closed on transient
656
+ errors too.
463
657
 
464
658
  ---
465
659