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 +302 -108
- package/dist/client.d.ts +112 -4
- package/dist/client.js +162 -8
- package/dist/extras/index.d.ts +49 -2
- package/dist/extras/index.js +90 -3
- package/dist/extras/langchain.d.ts +69 -18
- package/dist/extras/langchain.js +118 -35
- package/dist/extras/mcp.d.ts +24 -3
- package/dist/extras/mcp.js +75 -10
- package/dist/extras/openai-agents.d.ts +48 -16
- package/dist/extras/openai-agents.js +85 -29
- package/dist/extras/vercel-ai.d.ts +54 -17
- package/dist/extras/vercel-ai.js +104 -30
- package/dist/index.d.ts +1 -1
- package/dist/session.d.ts +15 -4
- package/dist/session.js +11 -3
- package/dist/types.d.ts +74 -20
- package/dist/types.js +15 -4
- package/package.json +2 -2
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
|
[](https://www.npmjs.com/package/aira-sdk)
|
|
6
4
|
[](https://opensource.org/licenses/MIT)
|
|
7
5
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
console.log(
|
|
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
|
-
|
|
165
|
+
Every write operation produces a cryptographic receipt.
|
|
51
166
|
|
|
52
167
|
| Category | Method | Description |
|
|
53
168
|
|---|---|---|
|
|
54
|
-
| **Actions** | `
|
|
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
|
-
| | `
|
|
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
|
-
| | `
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
312
|
+
const auth = await aira.authorize({
|
|
191
313
|
actionType: "api_call",
|
|
192
|
-
details: "
|
|
193
|
-
agentId: "
|
|
194
|
-
|
|
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 `
|
|
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
|
-
|
|
236
|
-
await sess.
|
|
237
|
-
|
|
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
|
|
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.
|
|
251
|
-
await aira.
|
|
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 —
|
|
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
|
|
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
|
|
415
|
+
const auth = await aira.authorize({
|
|
267
416
|
actionType: "loan_decision",
|
|
268
|
-
details: "
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 →
|
|
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)
|
|
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.
|
|
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.
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
|
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`
|
|
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(
|
|
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
|
-
|
|
352
|
-
|
|
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`
|
|
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(
|
|
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
|
-
|
|
367
|
-
const result = await middleware.wrapGenerateText({
|
|
542
|
+
const result = await generateText({
|
|
368
543
|
model: openai("gpt-5.2"),
|
|
369
|
-
prompt: "
|
|
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`
|
|
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(
|
|
563
|
+
const guardrail = new AiraGuardrail(aira, "assistant-agent");
|
|
383
564
|
|
|
384
|
-
|
|
385
|
-
const
|
|
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
|
|
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
|
|
399
|
-
|
|
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
|
|
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.
|
|
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); //
|
|
456
|
-
console.log(e.code); // "
|
|
457
|
-
console.log(e.message);
|
|
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
|
-
|
|
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
|
|