aira-sdk 1.0.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/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ActionReceipt, ActionDetail, AgentDetail, AgentVersion, EvidencePackage, ComplianceSnapshot, EscrowAccount, EscrowTransaction, VerifyResult, PaginatedList } from "./types";
1
+ import { Authorization, ActionReceipt, ActionDetail, AgentDetail, AgentVersion, CosignResult, EvidencePackage, ComplianceSnapshot, EscrowAccount, EscrowTransaction, VerifyResult, PaginatedList } from "./types";
2
2
  import { AiraSession } from "./session";
3
3
  export interface AiraOptions {
4
4
  apiKey: string;
@@ -18,19 +18,48 @@ export declare class Aira {
18
18
  private put;
19
19
  private del;
20
20
  private paginated;
21
- notarize(params: {
21
+ /**
22
+ * Step 1 — Authorize an action BEFORE it executes.
23
+ *
24
+ * Returns an `Authorization` with a status:
25
+ * - "authorized" → safe to execute the action, then call `notarize()`
26
+ * - "pending_approval" → enqueue `action_id` and wait for human approval
27
+ *
28
+ * If a policy denies the action, this throws `AiraError` with code
29
+ * `POLICY_DENIED` (HTTP 403). Duplicate idempotent requests throw
30
+ * `DUPLICATE_REQUEST` (HTTP 409).
31
+ */
32
+ authorize(params: {
22
33
  actionType: string;
23
34
  details: string;
24
35
  agentId?: string;
25
36
  agentVersion?: string;
37
+ instructionHash?: string;
26
38
  modelId?: string;
27
39
  modelVersion?: string;
28
- instructionHash?: string;
29
40
  parentActionId?: string;
41
+ endpointUrl?: string;
30
42
  storeDetails?: boolean;
31
43
  idempotencyKey?: string;
32
44
  requireApproval?: boolean;
33
45
  approvers?: string[];
46
+ systemPromptHash?: string;
47
+ toolInputsHash?: string;
48
+ modelParams?: Record<string, unknown>;
49
+ executionEnv?: Record<string, unknown>;
50
+ }): Promise<Authorization>;
51
+ /**
52
+ * Step 2 — Notarize the outcome of an already-authorized action.
53
+ *
54
+ * Call this AFTER executing the action. Outcome is "completed" by default;
55
+ * pass "failed" if the action ran but failed so the audit trail captures
56
+ * the failure. The returned `ActionReceipt` carries the Ed25519 signature
57
+ * and RFC 3161 timestamp token when the status is "notarized".
58
+ */
59
+ notarize(params: {
60
+ actionId: string;
61
+ outcome?: "completed" | "failed";
62
+ outcomeDetails?: string;
34
63
  }): Promise<ActionReceipt>;
35
64
  getAction(actionId: string): Promise<ActionDetail>;
36
65
  listActions(params?: {
@@ -40,7 +69,17 @@ export declare class Aira {
40
69
  agentId?: string;
41
70
  status?: string;
42
71
  }): Promise<PaginatedList<ActionDetail>>;
43
- authorizeAction(actionId: string): Promise<Record<string, unknown>>;
72
+ /**
73
+ * Add a human co-signature to an action that already exists.
74
+ *
75
+ * This is distinct from the authorization gate (it runs against `/cosign`,
76
+ * not `/authorize`). It records that a specific human has acknowledged or
77
+ * signed off on an action that was already authorized and notarized.
78
+ * Requires JWT auth (dashboard user, not an API key).
79
+ */
80
+ cosign(params: {
81
+ actionId: string;
82
+ }): Promise<CosignResult>;
44
83
  setLegalHold(actionId: string): Promise<Record<string, unknown>>;
45
84
  releaseLegalHold(actionId: string): Promise<Record<string, unknown>>;
46
85
  getActionChain(actionId: string): Promise<Record<string, unknown>[]>;
@@ -163,6 +202,75 @@ export declare class Aira {
163
202
  attestReputation(slug: string, counterpartyDid: string, actionId: string, attestation: string, signature: string): Promise<Record<string, unknown>>;
164
203
  /** Verify a reputation score by returning inputs and score_hash. */
165
204
  verifyReputation(slug: string): Promise<Record<string, unknown>>;
205
+ /**
206
+ * Get all reproducibility metadata stored for an action.
207
+ *
208
+ * Returns the system_prompt_hash, tool_inputs_hash, model_params,
209
+ * execution_env, and other knobs that an external replay tool
210
+ * needs to confirm it has the same inputs as the original run.
211
+ */
212
+ getReplayContext(actionId: string): Promise<Record<string, unknown>>;
213
+ /**
214
+ * Seal a regulator-ready evidence bundle for a date range.
215
+ *
216
+ * `framework` must be one of: `eu_ai_act_art12`, `iso_42001`,
217
+ * `aiuc_1`, `soc_2_cc7`, `raw`.
218
+ */
219
+ createComplianceBundle(params: {
220
+ framework: "eu_ai_act_art12" | "iso_42001" | "aiuc_1" | "soc_2_cc7" | "raw";
221
+ periodStart: string;
222
+ periodEnd: string;
223
+ title?: string;
224
+ agentFilter?: string[];
225
+ /**
226
+ * Client-supplied key (unique per org) — retrying with the same key
227
+ * returns the original bundle and does NOT charge a second operation.
228
+ * Use this if your job runner may replay the call on network flakes.
229
+ */
230
+ idempotencyKey?: string;
231
+ }): Promise<Record<string, unknown>>;
232
+ listComplianceBundles(page?: number, perPage?: number): Promise<PaginatedList<Record<string, unknown>>>;
233
+ getComplianceBundle(bundleId: string): Promise<Record<string, unknown>>;
234
+ /**
235
+ * Download the self-contained JSON document for the bundle. The
236
+ * exported document inlines every receipt's signed payload + signature
237
+ * and the JWKS URL so an auditor can re-verify offline.
238
+ */
239
+ exportComplianceBundle(bundleId: string): Promise<Record<string, unknown>>;
240
+ getBundleInclusionProof(bundleId: string, receiptId: string): Promise<Record<string, unknown>>;
241
+ /**
242
+ * Score the agent's recent behavior against its active baseline.
243
+ * Read-only — does NOT persist an alert. Use this for dashboards.
244
+ */
245
+ getDriftStatus(agentId: string, lookbackHours?: number): Promise<Record<string, unknown>>;
246
+ /** Compute a behavioral baseline from production action history. */
247
+ computeDriftBaseline(params: {
248
+ agentId: string;
249
+ windowStart: string;
250
+ windowEnd: string;
251
+ activate?: boolean;
252
+ }): Promise<Record<string, unknown>>;
253
+ /** Seed a baseline from a config dict (for cold-start agents). */
254
+ seedSyntheticBaseline(params: {
255
+ agentId: string;
256
+ expectedDistribution: Record<string, number>;
257
+ expectedActionsPerDay: number;
258
+ activate?: boolean;
259
+ }): Promise<Record<string, unknown>>;
260
+ /** Score the current window and persist an alert if it exceeds the threshold. */
261
+ runDriftCheck(agentId: string, lookbackHours?: number): Promise<Record<string, unknown> | null>;
262
+ listDriftAlerts(agentId: string, page?: number, acknowledged?: boolean): Promise<PaginatedList<Record<string, unknown>>>;
263
+ acknowledgeDriftAlert(agentId: string, alertId: string): Promise<Record<string, unknown>>;
264
+ /**
265
+ * Seal every unsettled receipt for the org into a new settlement.
266
+ * Admin-only. Returns the new settlement, or null if there were no
267
+ * unsettled receipts (no-op).
268
+ */
269
+ createSettlement(): Promise<Record<string, unknown> | null>;
270
+ listSettlements(page?: number, perPage?: number): Promise<PaginatedList<Record<string, unknown>>>;
271
+ getSettlement(settlementId: string): Promise<Record<string, unknown>>;
272
+ /** Get the Merkle inclusion proof for one receipt in its settlement. */
273
+ getSettlementInclusionProof(receiptId: string): Promise<Record<string, unknown>>;
166
274
  /** Create a scoped session with pre-filled defaults. */
167
275
  session(agentId: string, defaults?: Record<string, unknown>): AiraSession;
168
276
  /** Number of queued offline requests. */
package/dist/client.js CHANGED
@@ -46,7 +46,8 @@ class Aira {
46
46
  return {};
47
47
  const data = (await res.json().catch(() => ({ error: res.statusText, code: "UNKNOWN" })));
48
48
  if (!res.ok) {
49
- throw new types_1.AiraError(res.status, data.code ?? "UNKNOWN", data.error ?? res.statusText);
49
+ const message = data.message ?? res.statusText;
50
+ throw new types_1.AiraError(res.status, data.code ?? "UNKNOWN", message, data.details ?? {});
50
51
  }
51
52
  return data;
52
53
  }
@@ -86,24 +87,55 @@ class Aira {
86
87
  const p = data.pagination;
87
88
  return { data: data.data, total: p.total, page: p.page, per_page: p.per_page, has_more: p.has_more };
88
89
  }
89
- // ==================== Actions ====================
90
- async notarize(params) {
90
+ // ==================== Actions (two-step: authorize → notarize) ====================
91
+ /**
92
+ * Step 1 — Authorize an action BEFORE it executes.
93
+ *
94
+ * Returns an `Authorization` with a status:
95
+ * - "authorized" → safe to execute the action, then call `notarize()`
96
+ * - "pending_approval" → enqueue `action_id` and wait for human approval
97
+ *
98
+ * If a policy denies the action, this throws `AiraError` with code
99
+ * `POLICY_DENIED` (HTTP 403). Duplicate idempotent requests throw
100
+ * `DUPLICATE_REQUEST` (HTTP 409).
101
+ */
102
+ async authorize(params) {
91
103
  const body = buildBody({
92
104
  action_type: params.actionType,
93
105
  details: truncateDetails(params.details),
94
106
  agent_id: params.agentId,
95
107
  agent_version: params.agentVersion,
108
+ instruction_hash: params.instructionHash,
96
109
  model_id: params.modelId,
97
110
  model_version: params.modelVersion,
98
- instruction_hash: params.instructionHash,
99
111
  parent_action_id: params.parentActionId,
112
+ endpoint_url: params.endpointUrl,
100
113
  store_details: params.storeDetails || undefined,
101
114
  idempotency_key: params.idempotencyKey,
102
115
  require_approval: params.requireApproval || undefined,
103
116
  approvers: params.approvers,
117
+ system_prompt_hash: params.systemPromptHash,
118
+ tool_inputs_hash: params.toolInputsHash,
119
+ model_params: params.modelParams,
120
+ execution_env: params.executionEnv,
104
121
  });
105
122
  return this.post("/actions", body);
106
123
  }
124
+ /**
125
+ * Step 2 — Notarize the outcome of an already-authorized action.
126
+ *
127
+ * Call this AFTER executing the action. Outcome is "completed" by default;
128
+ * pass "failed" if the action ran but failed so the audit trail captures
129
+ * the failure. The returned `ActionReceipt` carries the Ed25519 signature
130
+ * and RFC 3161 timestamp token when the status is "notarized".
131
+ */
132
+ async notarize(params) {
133
+ const body = buildBody({
134
+ outcome: params.outcome ?? "completed",
135
+ outcome_details: params.outcomeDetails,
136
+ });
137
+ return this.post(`/actions/${params.actionId}/notarize`, body);
138
+ }
107
139
  async getAction(actionId) {
108
140
  return this.get(`/actions/${actionId}`);
109
141
  }
@@ -113,8 +145,16 @@ class Aira {
113
145
  }));
114
146
  return this.paginated(data);
115
147
  }
116
- async authorizeAction(actionId) {
117
- return this.post(`/actions/${actionId}/authorize`, {});
148
+ /**
149
+ * Add a human co-signature to an action that already exists.
150
+ *
151
+ * This is distinct from the authorization gate (it runs against `/cosign`,
152
+ * not `/authorize`). It records that a specific human has acknowledged or
153
+ * signed off on an action that was already authorized and notarized.
154
+ * Requires JWT auth (dashboard user, not an API key).
155
+ */
156
+ async cosign(params) {
157
+ return this.post(`/actions/${params.actionId}/cosign`, {});
118
158
  }
119
159
  async setLegalHold(actionId) {
120
160
  return this.post(`/actions/${actionId}/hold`, {});
@@ -341,6 +381,112 @@ class Aira {
341
381
  async verifyReputation(slug) {
342
382
  return this.get(`/agents/${slug}/reputation/verify`);
343
383
  }
384
+ // ==================== Replay context (F10) ====================
385
+ /**
386
+ * Get all reproducibility metadata stored for an action.
387
+ *
388
+ * Returns the system_prompt_hash, tool_inputs_hash, model_params,
389
+ * execution_env, and other knobs that an external replay tool
390
+ * needs to confirm it has the same inputs as the original run.
391
+ */
392
+ async getReplayContext(actionId) {
393
+ return this.get(`/actions/${actionId}/replay-context`);
394
+ }
395
+ // ==================== Compliance bundles ====================
396
+ /**
397
+ * Seal a regulator-ready evidence bundle for a date range.
398
+ *
399
+ * `framework` must be one of: `eu_ai_act_art12`, `iso_42001`,
400
+ * `aiuc_1`, `soc_2_cc7`, `raw`.
401
+ */
402
+ async createComplianceBundle(params) {
403
+ const body = buildBody({
404
+ framework: params.framework,
405
+ period_start: params.periodStart,
406
+ period_end: params.periodEnd,
407
+ title: params.title,
408
+ agent_filter: params.agentFilter,
409
+ idempotency_key: params.idempotencyKey,
410
+ });
411
+ return this.post("/compliance/bundles", body);
412
+ }
413
+ async listComplianceBundles(page = 1, perPage = 20) {
414
+ const data = await this.get(`/compliance/bundles?page=${page}&per_page=${perPage}`);
415
+ return this.paginated(data);
416
+ }
417
+ async getComplianceBundle(bundleId) {
418
+ return this.get(`/compliance/bundles/${bundleId}`);
419
+ }
420
+ /**
421
+ * Download the self-contained JSON document for the bundle. The
422
+ * exported document inlines every receipt's signed payload + signature
423
+ * and the JWKS URL so an auditor can re-verify offline.
424
+ */
425
+ async exportComplianceBundle(bundleId) {
426
+ return this.get(`/compliance/bundles/${bundleId}/export`);
427
+ }
428
+ async getBundleInclusionProof(bundleId, receiptId) {
429
+ return this.get(`/compliance/bundles/${bundleId}/inclusion-proof/${receiptId}`);
430
+ }
431
+ // ==================== Drift detection ====================
432
+ /**
433
+ * Score the agent's recent behavior against its active baseline.
434
+ * Read-only — does NOT persist an alert. Use this for dashboards.
435
+ */
436
+ async getDriftStatus(agentId, lookbackHours = 24) {
437
+ return this.get(`/agents/${agentId}/drift?lookback_hours=${lookbackHours}`);
438
+ }
439
+ /** Compute a behavioral baseline from production action history. */
440
+ async computeDriftBaseline(params) {
441
+ return this.post(`/agents/${params.agentId}/drift/baseline`, buildBody({
442
+ window_start: params.windowStart,
443
+ window_end: params.windowEnd,
444
+ activate: params.activate ?? true,
445
+ }));
446
+ }
447
+ /** Seed a baseline from a config dict (for cold-start agents). */
448
+ async seedSyntheticBaseline(params) {
449
+ return this.post(`/agents/${params.agentId}/drift/baseline/synthetic`, buildBody({
450
+ expected_distribution: params.expectedDistribution,
451
+ expected_actions_per_day: params.expectedActionsPerDay,
452
+ activate: params.activate ?? true,
453
+ }));
454
+ }
455
+ /** Score the current window and persist an alert if it exceeds the threshold. */
456
+ async runDriftCheck(agentId, lookbackHours = 24) {
457
+ return this.post(`/agents/${agentId}/drift/check?lookback_hours=${lookbackHours}`, {});
458
+ }
459
+ async listDriftAlerts(agentId, page = 1, acknowledged) {
460
+ let params = `page=${page}&per_page=50`;
461
+ if (acknowledged !== undefined) {
462
+ params += `&acknowledged=${acknowledged}`;
463
+ }
464
+ const data = await this.get(`/agents/${agentId}/drift/alerts?${params}`);
465
+ return this.paginated(data);
466
+ }
467
+ async acknowledgeDriftAlert(agentId, alertId) {
468
+ return this.post(`/agents/${agentId}/drift/alerts/${alertId}/acknowledge`, {});
469
+ }
470
+ // ==================== Merkle settlement (F8) ====================
471
+ /**
472
+ * Seal every unsettled receipt for the org into a new settlement.
473
+ * Admin-only. Returns the new settlement, or null if there were no
474
+ * unsettled receipts (no-op).
475
+ */
476
+ async createSettlement() {
477
+ return this.post("/settlements", {});
478
+ }
479
+ async listSettlements(page = 1, perPage = 20) {
480
+ const data = await this.get(`/settlements?page=${page}&per_page=${perPage}`);
481
+ return this.paginated(data);
482
+ }
483
+ async getSettlement(settlementId) {
484
+ return this.get(`/settlements/${settlementId}`);
485
+ }
486
+ /** Get the Merkle inclusion proof for one receipt in its settlement. */
487
+ async getSettlementInclusionProof(receiptId) {
488
+ return this.get(`/settlements/inclusion-proof/${receiptId}`);
489
+ }
344
490
  // ==================== Session ====================
345
491
  /** Create a scoped session with pre-filled defaults. */
346
492
  session(agentId, defaults) {
@@ -1,14 +1,30 @@
1
1
  /**
2
2
  * Aira SDK extras — framework integrations.
3
3
  *
4
- * Each integration is in its own file to avoid importing unnecessary dependencies.
5
- * Import directly from the subpath:
4
+ * Each integration is in its own file to avoid importing unnecessary
5
+ * dependencies. Import directly from the subpath:
6
6
  *
7
7
  * import { AiraCallbackHandler } from "aira-sdk/extras/langchain";
8
8
  * import { AiraVercelMiddleware } from "aira-sdk/extras/vercel-ai";
9
9
  * import { AiraGuardrail } from "aira-sdk/extras/openai-agents";
10
10
  * import { createServer } from "aira-sdk/extras/mcp";
11
11
  * import { verifySignature, parseEvent } from "aira-sdk/extras/webhooks";
12
+ *
13
+ * Every integration is honestly labeled as one of three kinds:
14
+ *
15
+ * "gate" — intercepts before execution and can deny. authorize()
16
+ * runs first; if the policy engine denies, the wrapped
17
+ * call never runs.
18
+ * "audit" — runs after execution because the host framework does not
19
+ * expose a pre-execution hook that can abort. Aira still
20
+ * records a signed receipt; it just cannot prevent the
21
+ * action.
22
+ * "adapter" — exposes Aira's own API as a tool the host framework can
23
+ * call. Neither a gate nor an audit hook over other tools.
24
+ *
25
+ * The INTEGRATIONS registry below is the single source of truth — the
26
+ * README integration matrix is generated from it so the docs cannot
27
+ * drift from the code.
12
28
  */
13
29
  export { AiraCallbackHandler } from "./langchain";
14
30
  export { AiraVercelMiddleware } from "./vercel-ai";
@@ -19,3 +35,34 @@ export { verifySignature, parseEvent, WebhookEventType } from "./webhooks";
19
35
  export type { WebhookEvent, WebhookEventTypeName } from "./webhooks";
20
36
  export { checkTrust } from "./trust";
21
37
  export type { TrustPolicy, TrustContext } from "./trust";
38
+ export type IntegrationKind = "gate" | "audit" | "adapter";
39
+ export interface IntegrationSpec {
40
+ /** Human display name. */
41
+ name: string;
42
+ /** SDK subpath: aira-sdk/extras/{module}. */
43
+ module: string;
44
+ /** Primary exported symbol. */
45
+ symbol: string;
46
+ /** Honest classification. */
47
+ kind: IntegrationKind;
48
+ /**
49
+ * True if Aira can intercept and deny BEFORE the underlying call runs.
50
+ * Must be true for kind=="gate" and false otherwise.
51
+ */
52
+ preExecutionGate: boolean;
53
+ /** What the integration wraps. */
54
+ surface: string;
55
+ /** Why this is gate / audit / adapter — surface in docs and tests. */
56
+ notes: string;
57
+ }
58
+ /**
59
+ * The single source of truth for the integration matrix. Tests pin this
60
+ * registry; the README is generated from it. To add a new integration:
61
+ *
62
+ * 1. Implement the file under src/extras/
63
+ * 2. Add an IntegrationSpec entry here
64
+ * 3. Run `npm test` — failing tests will tell you what to update
65
+ */
66
+ export declare const INTEGRATIONS: readonly IntegrationSpec[];
67
+ /** Render INTEGRATIONS as a Markdown table for the README. */
68
+ export declare function integrationMatrixMarkdown(): string;
@@ -2,17 +2,34 @@
2
2
  /**
3
3
  * Aira SDK extras — framework integrations.
4
4
  *
5
- * Each integration is in its own file to avoid importing unnecessary dependencies.
6
- * Import directly from the subpath:
5
+ * Each integration is in its own file to avoid importing unnecessary
6
+ * dependencies. Import directly from the subpath:
7
7
  *
8
8
  * import { AiraCallbackHandler } from "aira-sdk/extras/langchain";
9
9
  * import { AiraVercelMiddleware } from "aira-sdk/extras/vercel-ai";
10
10
  * import { AiraGuardrail } from "aira-sdk/extras/openai-agents";
11
11
  * import { createServer } from "aira-sdk/extras/mcp";
12
12
  * import { verifySignature, parseEvent } from "aira-sdk/extras/webhooks";
13
+ *
14
+ * Every integration is honestly labeled as one of three kinds:
15
+ *
16
+ * "gate" — intercepts before execution and can deny. authorize()
17
+ * runs first; if the policy engine denies, the wrapped
18
+ * call never runs.
19
+ * "audit" — runs after execution because the host framework does not
20
+ * expose a pre-execution hook that can abort. Aira still
21
+ * records a signed receipt; it just cannot prevent the
22
+ * action.
23
+ * "adapter" — exposes Aira's own API as a tool the host framework can
24
+ * call. Neither a gate nor an audit hook over other tools.
25
+ *
26
+ * The INTEGRATIONS registry below is the single source of truth — the
27
+ * README integration matrix is generated from it so the docs cannot
28
+ * drift from the code.
13
29
  */
14
30
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.checkTrust = exports.WebhookEventType = exports.parseEvent = exports.verifySignature = exports.handleToolCall = exports.getTools = exports.createServer = exports.AiraGuardrail = exports.AiraVercelMiddleware = exports.AiraCallbackHandler = void 0;
31
+ exports.INTEGRATIONS = exports.checkTrust = exports.WebhookEventType = exports.parseEvent = exports.verifySignature = exports.handleToolCall = exports.getTools = exports.createServer = exports.AiraGuardrail = exports.AiraVercelMiddleware = exports.AiraCallbackHandler = void 0;
32
+ exports.integrationMatrixMarkdown = integrationMatrixMarkdown;
16
33
  var langchain_1 = require("./langchain");
17
34
  Object.defineProperty(exports, "AiraCallbackHandler", { enumerable: true, get: function () { return langchain_1.AiraCallbackHandler; } });
18
35
  var vercel_ai_1 = require("./vercel-ai");
@@ -29,3 +46,73 @@ Object.defineProperty(exports, "parseEvent", { enumerable: true, get: function (
29
46
  Object.defineProperty(exports, "WebhookEventType", { enumerable: true, get: function () { return webhooks_1.WebhookEventType; } });
30
47
  var trust_1 = require("./trust");
31
48
  Object.defineProperty(exports, "checkTrust", { enumerable: true, get: function () { return trust_1.checkTrust; } });
49
+ /**
50
+ * The single source of truth for the integration matrix. Tests pin this
51
+ * registry; the README is generated from it. To add a new integration:
52
+ *
53
+ * 1. Implement the file under src/extras/
54
+ * 2. Add an IntegrationSpec entry here
55
+ * 3. Run `npm test` — failing tests will tell you what to update
56
+ */
57
+ exports.INTEGRATIONS = [
58
+ {
59
+ name: "LangChain.js",
60
+ module: "langchain",
61
+ symbol: "AiraCallbackHandler",
62
+ kind: "gate",
63
+ preExecutionGate: true,
64
+ surface: "Tools (gate). Chains and LLM completions are audit-only.",
65
+ notes: "handleToolStart calls authorize() and throws on POLICY_DENIED so the " +
66
+ "tool never runs. Chain/LLM hooks are post-hoc because LangChain has " +
67
+ "no pre-execution chain hook that can abort.",
68
+ },
69
+ {
70
+ name: "Vercel AI SDK",
71
+ module: "vercel-ai",
72
+ symbol: "AiraVercelMiddleware",
73
+ kind: "gate",
74
+ preExecutionGate: true,
75
+ surface: "Tools via wrapTool() (gate). onFinish helpers are audit-only.",
76
+ notes: "wrapTool() wraps a tool's execute function so authorize() runs " +
77
+ "before the tool body. onStepFinish / onFinish callbacks fire after " +
78
+ "execution and are explicitly labeled audit-only — Vercel AI has no " +
79
+ "pre-step hook.",
80
+ },
81
+ {
82
+ name: "OpenAI Agents",
83
+ module: "openai-agents",
84
+ symbol: "AiraGuardrail",
85
+ kind: "gate",
86
+ preExecutionGate: true,
87
+ surface: "Tools via wrapTool()",
88
+ notes: "Wraps each tool function: authorize() runs before the tool body. " +
89
+ "Denied calls throw; failed calls notarize with outcome=failed.",
90
+ },
91
+ {
92
+ name: "MCP",
93
+ module: "mcp",
94
+ symbol: "createServer",
95
+ kind: "adapter",
96
+ preExecutionGate: false,
97
+ surface: "Server adapter (exposes Aira as MCP tools)",
98
+ notes: "MCP is bidirectional: the agent CHOOSES to call authorize_action / " +
99
+ "notarize_action. This is not a wrapper over other MCP tools — it is " +
100
+ "a protocol adapter that lets MCP-aware agents reach Aira.",
101
+ },
102
+ {
103
+ name: "Webhooks",
104
+ module: "webhooks",
105
+ symbol: "verifySignature",
106
+ kind: "adapter",
107
+ preExecutionGate: false,
108
+ surface: "HMAC-SHA256 webhook signature verifier",
109
+ notes: "Standalone HMAC verification helper. Not an agent integration.",
110
+ },
111
+ ];
112
+ /** Render INTEGRATIONS as a Markdown table for the README. */
113
+ function integrationMatrixMarkdown() {
114
+ const header = "| Integration | Type | Pre-execution gate? | Surface | Notes |\n" +
115
+ "|---|---|---|---|---|";
116
+ const rows = exports.INTEGRATIONS.map((i) => `| **${i.name}** | ${i.kind} | ${i.preExecutionGate ? "Yes" : "No"} | ${i.surface} | ${i.notes} |`);
117
+ return [header, ...rows].join("\n");
118
+ }
@@ -1,42 +1,93 @@
1
1
  /**
2
- * LangChain.js integration — auto-notarize tool and chain completions.
2
+ * LangChain.js integration — pre-execution gate + post-execution notarize.
3
3
  *
4
4
  * Requires: @langchain/core (peer dependency)
5
5
  *
6
- * Usage:
7
- * import { AiraCallbackHandler } from "aira-sdk/extras/langchain";
8
- * const handler = new AiraCallbackHandler(aira, "my-agent");
9
- * const chain = someChain.withConfig({ callbacks: [handler] });
6
+ * ---------------------------------------------------------------------------
7
+ * LIFECYCLE & DESIGN NOTES
8
+ * ---------------------------------------------------------------------------
9
+ *
10
+ * LangChain fires callbacks BEFORE and AFTER each tool/chain/LLM step. The
11
+ * `handleToolStart` / `handleChainStart` / `handleLLMStart` callbacks are
12
+ * genuine pre-execution hooks: if one throws, LangChain propagates the error
13
+ * and the tool is never executed. That means `handleToolStart` can serve as
14
+ * a real authorization gate — not merely an audit hook.
15
+ *
16
+ * This handler implements the two-step flow as follows:
17
+ *
18
+ * 1. handleToolStart → aira.authorize()
19
+ * - If the backend returns "authorized" we cache the action_id
20
+ * keyed by LangChain's `runId`, then return so the tool executes.
21
+ * - If the backend throws POLICY_DENIED we propagate the error,
22
+ * which prevents the tool from running at all (real gate).
23
+ * - If the backend returns "pending_approval" we throw an error
24
+ * so the tool does NOT execute until a human approves.
25
+ *
26
+ * 2. handleToolEnd / handleToolError → aira.notarize()
27
+ * - Notarize the outcome as "completed" or "failed". This closes
28
+ * the two-step flow and produces a cryptographic receipt.
29
+ *
30
+ * The same pattern applies to chains and LLM calls. For chains and LLMs we
31
+ * use "chain_run" / "llm_run" as action types so you can filter by them.
32
+ *
33
+ * If the integration cannot reach Aira at authorize time, it fails open with
34
+ * a console warning — your agent keeps running, but no receipt is produced.
35
+ * To make it fail closed, set `strict: true` in the options.
10
36
  */
11
37
  import type { Aira } from "../client";
12
38
  import type { TrustPolicy, TrustContext } from "./trust";
13
39
  export type { TrustPolicy, TrustContext } from "./trust";
40
+ export interface AiraCallbackHandlerOptions {
41
+ modelId?: string;
42
+ actionTypes?: Record<string, string>;
43
+ trustPolicy?: TrustPolicy;
44
+ /** Fail closed if authorize() fails (network, 5xx). Default: false. */
45
+ strict?: boolean;
46
+ }
14
47
  export declare class AiraCallbackHandler {
15
48
  private client;
16
49
  private agentId;
17
50
  private modelId?;
18
51
  private actionTypes;
19
52
  private trustPolicy?;
20
- constructor(client: Aira, agentId: string, options?: {
21
- modelId?: string;
22
- actionTypes?: Record<string, string>;
23
- trustPolicy?: TrustPolicy;
24
- });
53
+ private strict;
54
+ /** runId → action_id cache so handleEnd can notarize the right action. */
55
+ private inFlight;
56
+ constructor(client: Aira, agentId: string, options?: AiraCallbackHandlerOptions);
25
57
  /**
26
58
  * Check trust for a counterparty agent before interacting.
27
59
  * Advisory by default — only blocks on revoked VC or unregistered agent if configured.
28
60
  */
29
61
  checkTrust(counterpartyId: string): Promise<TrustContext>;
30
- private notarize;
31
- /** Called when a tool finishes. */
32
- handleToolEnd(output: string, name?: string): void;
33
- /** Called when a chain finishes. */
34
- handleChainEnd(outputs: Record<string, unknown>): void;
35
- /** Called when an LLM finishes. */
36
- handleLLMEnd(generationCount: number): void;
62
+ private doAuthorize;
63
+ private doNotarize;
64
+ /** Called BEFORE a tool runs — authorization gate. */
65
+ handleToolStart(tool: {
66
+ name?: string;
67
+ } | string | unknown, input: string, runId: string): Promise<void>;
68
+ /** Called AFTER a tool completes successfully. */
69
+ handleToolEnd(output: string, runId: string, name?: string): Promise<void>;
70
+ /** Called if a tool throws. */
71
+ handleToolError(err: Error, runId: string, name?: string): Promise<void>;
72
+ /** Called BEFORE a chain runs. */
73
+ handleChainStart(chain: {
74
+ name?: string;
75
+ } | unknown, inputs: Record<string, unknown>, runId: string): Promise<void>;
76
+ /** Called AFTER a chain completes. */
77
+ handleChainEnd(outputs: Record<string, unknown>, runId: string): Promise<void>;
78
+ /** Called if a chain throws. */
79
+ handleChainError(err: Error, runId: string): Promise<void>;
80
+ /** Called BEFORE an LLM runs. */
81
+ handleLLMStart(llm: unknown, prompts: string[], runId: string): Promise<void>;
82
+ /** Called AFTER an LLM completes. */
83
+ handleLLMEnd(response: {
84
+ generations?: unknown[];
85
+ } | number, runId: string): Promise<void>;
86
+ /** Called if an LLM throws. */
87
+ handleLLMError(err: Error, runId: string): Promise<void>;
37
88
  /**
38
89
  * Returns a LangChain-compatible callbacks object.
39
90
  * Use with: chain.invoke(input, { callbacks: [handler.asCallbacks()] })
40
91
  */
41
- asCallbacks(): Record<string, (...args: unknown[]) => void>;
92
+ asCallbacks(): Record<string, (...args: unknown[]) => Promise<void> | void>;
42
93
  }