aira-sdk 1.0.0 → 2.4.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 +300 -104
- package/dist/client.d.ts +195 -4
- package/dist/client.js +347 -6
- 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 +70 -9
- 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/index.js +5 -1
- package/dist/session.d.ts +15 -4
- package/dist/session.js +11 -3
- package/dist/types.d.ts +197 -20
- package/dist/types.js +26 -5
- package/package.json +1 -1
package/dist/extras/index.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/extras/index.js
CHANGED
|
@@ -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
|
|
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 —
|
|
2
|
+
* LangChain.js integration — pre-execution gate + post-execution notarize.
|
|
3
3
|
*
|
|
4
4
|
* Requires: @langchain/core (peer dependency)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
}
|
package/dist/extras/langchain.js
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* LangChain.js integration —
|
|
3
|
+
* LangChain.js integration — pre-execution gate + post-execution notarize.
|
|
4
4
|
*
|
|
5
5
|
* Requires: @langchain/core (peer dependency)
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
* LIFECYCLE & DESIGN NOTES
|
|
9
|
+
* ---------------------------------------------------------------------------
|
|
10
|
+
*
|
|
11
|
+
* LangChain fires callbacks BEFORE and AFTER each tool/chain/LLM step. The
|
|
12
|
+
* `handleToolStart` / `handleChainStart` / `handleLLMStart` callbacks are
|
|
13
|
+
* genuine pre-execution hooks: if one throws, LangChain propagates the error
|
|
14
|
+
* and the tool is never executed. That means `handleToolStart` can serve as
|
|
15
|
+
* a real authorization gate — not merely an audit hook.
|
|
16
|
+
*
|
|
17
|
+
* This handler implements the two-step flow as follows:
|
|
18
|
+
*
|
|
19
|
+
* 1. handleToolStart → aira.authorize()
|
|
20
|
+
* - If the backend returns "authorized" we cache the action_id
|
|
21
|
+
* keyed by LangChain's `runId`, then return so the tool executes.
|
|
22
|
+
* - If the backend throws POLICY_DENIED we propagate the error,
|
|
23
|
+
* which prevents the tool from running at all (real gate).
|
|
24
|
+
* - If the backend returns "pending_approval" we throw an error
|
|
25
|
+
* so the tool does NOT execute until a human approves.
|
|
26
|
+
*
|
|
27
|
+
* 2. handleToolEnd / handleToolError → aira.notarize()
|
|
28
|
+
* - Notarize the outcome as "completed" or "failed". This closes
|
|
29
|
+
* the two-step flow and produces a cryptographic receipt.
|
|
30
|
+
*
|
|
31
|
+
* The same pattern applies to chains and LLM calls. For chains and LLMs we
|
|
32
|
+
* use "chain_run" / "llm_run" as action types so you can filter by them.
|
|
33
|
+
*
|
|
34
|
+
* If the integration cannot reach Aira at authorize time, it fails open with
|
|
35
|
+
* a console warning — your agent keeps running, but no receipt is produced.
|
|
36
|
+
* To make it fail closed, set `strict: true` in the options.
|
|
11
37
|
*/
|
|
12
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
39
|
exports.AiraCallbackHandler = void 0;
|
|
@@ -19,15 +45,19 @@ class AiraCallbackHandler {
|
|
|
19
45
|
modelId;
|
|
20
46
|
actionTypes;
|
|
21
47
|
trustPolicy;
|
|
48
|
+
strict;
|
|
49
|
+
/** runId → action_id cache so handleEnd can notarize the right action. */
|
|
50
|
+
inFlight = new Map();
|
|
22
51
|
constructor(client, agentId, options) {
|
|
23
52
|
this.client = client;
|
|
24
53
|
this.agentId = agentId;
|
|
25
54
|
this.modelId = options?.modelId;
|
|
26
55
|
this.trustPolicy = options?.trustPolicy;
|
|
56
|
+
this.strict = options?.strict ?? false;
|
|
27
57
|
this.actionTypes = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
58
|
+
tool: "tool_call",
|
|
59
|
+
chain: "chain_run",
|
|
60
|
+
llm: "llm_run",
|
|
31
61
|
...(options?.actionTypes ?? {}),
|
|
32
62
|
};
|
|
33
63
|
}
|
|
@@ -41,35 +71,88 @@ class AiraCallbackHandler {
|
|
|
41
71
|
}
|
|
42
72
|
return (0, trust_1.checkTrust)(this.client, this.trustPolicy, counterpartyId);
|
|
43
73
|
}
|
|
44
|
-
|
|
74
|
+
async doAuthorize(actionType, details, runId) {
|
|
45
75
|
try {
|
|
46
|
-
const
|
|
76
|
+
const auth = await this.client.authorize({
|
|
47
77
|
actionType,
|
|
48
78
|
details: details.slice(0, MAX_DETAILS),
|
|
49
79
|
agentId: this.agentId,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
modelId: this.modelId,
|
|
81
|
+
});
|
|
82
|
+
if (auth.status === "pending_approval") {
|
|
83
|
+
// Real gate — block the tool from running until a human approves.
|
|
84
|
+
const err = new Error(`Aira: action '${actionType}' is pending human approval (action_id=${auth.action_id}). Tool execution blocked.`);
|
|
85
|
+
err.code = "PENDING_APPROVAL";
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
this.inFlight.set(runId, auth.action_id);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
const err = e;
|
|
92
|
+
// Always propagate authorization-layer rejections.
|
|
93
|
+
if (err.code === "POLICY_DENIED" || err.code === "PENDING_APPROVAL")
|
|
94
|
+
throw e;
|
|
95
|
+
if (this.strict)
|
|
96
|
+
throw e;
|
|
97
|
+
console.warn("Aira authorize failed (fail-open):", err);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async doNotarize(runId, outcome, details) {
|
|
101
|
+
const actionId = this.inFlight.get(runId);
|
|
102
|
+
if (!actionId)
|
|
103
|
+
return;
|
|
104
|
+
this.inFlight.delete(runId);
|
|
105
|
+
try {
|
|
106
|
+
await this.client.notarize({
|
|
107
|
+
actionId,
|
|
108
|
+
outcome,
|
|
109
|
+
outcomeDetails: details.slice(0, MAX_DETAILS),
|
|
55
110
|
});
|
|
56
111
|
}
|
|
57
112
|
catch (e) {
|
|
58
113
|
console.warn("Aira notarize failed (non-blocking):", e);
|
|
59
114
|
}
|
|
60
115
|
}
|
|
61
|
-
/** Called
|
|
62
|
-
|
|
63
|
-
|
|
116
|
+
/** Called BEFORE a tool runs — authorization gate. */
|
|
117
|
+
async handleToolStart(tool, input, runId) {
|
|
118
|
+
const name = typeof tool === "string" ? tool : tool?.name ?? "unknown";
|
|
119
|
+
await this.doAuthorize(this.actionTypes.tool, `Tool '${name}' invoked. Input length: ${String(input).length} chars`, runId);
|
|
120
|
+
}
|
|
121
|
+
/** Called AFTER a tool completes successfully. */
|
|
122
|
+
async handleToolEnd(output, runId, name = "unknown") {
|
|
123
|
+
await this.doNotarize(runId, "completed", `Tool '${name}' completed. Output length: ${String(output).length} chars`);
|
|
124
|
+
}
|
|
125
|
+
/** Called if a tool throws. */
|
|
126
|
+
async handleToolError(err, runId, name = "unknown") {
|
|
127
|
+
await this.doNotarize(runId, "failed", `Tool '${name}' failed: ${err?.message ?? String(err)}`);
|
|
128
|
+
}
|
|
129
|
+
/** Called BEFORE a chain runs. */
|
|
130
|
+
async handleChainStart(chain, inputs, runId) {
|
|
131
|
+
const name = chain?.name ?? "chain";
|
|
132
|
+
const keys = typeof inputs === "object" && inputs ? Object.keys(inputs) : [];
|
|
133
|
+
await this.doAuthorize(this.actionTypes.chain, `Chain '${name}' started. Input keys: [${keys.join(", ")}]`, runId);
|
|
64
134
|
}
|
|
65
|
-
/** Called
|
|
66
|
-
handleChainEnd(outputs) {
|
|
135
|
+
/** Called AFTER a chain completes. */
|
|
136
|
+
async handleChainEnd(outputs, runId) {
|
|
67
137
|
const keys = typeof outputs === "object" && outputs ? Object.keys(outputs) : [];
|
|
68
|
-
this.
|
|
138
|
+
await this.doNotarize(runId, "completed", `Chain completed. Output keys: [${keys.join(", ")}]`);
|
|
69
139
|
}
|
|
70
|
-
/** Called
|
|
71
|
-
|
|
72
|
-
this.
|
|
140
|
+
/** Called if a chain throws. */
|
|
141
|
+
async handleChainError(err, runId) {
|
|
142
|
+
await this.doNotarize(runId, "failed", `Chain failed: ${err?.message ?? String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
/** Called BEFORE an LLM runs. */
|
|
145
|
+
async handleLLMStart(llm, prompts, runId) {
|
|
146
|
+
await this.doAuthorize(this.actionTypes.llm, `LLM called with ${prompts?.length ?? 0} prompt(s)`, runId);
|
|
147
|
+
}
|
|
148
|
+
/** Called AFTER an LLM completes. */
|
|
149
|
+
async handleLLMEnd(response, runId) {
|
|
150
|
+
const count = typeof response === "number" ? response : response?.generations?.length ?? 0;
|
|
151
|
+
await this.doNotarize(runId, "completed", `LLM completed. Generations: ${count}`);
|
|
152
|
+
}
|
|
153
|
+
/** Called if an LLM throws. */
|
|
154
|
+
async handleLLMError(err, runId) {
|
|
155
|
+
await this.doNotarize(runId, "failed", `LLM failed: ${err?.message ?? String(err)}`);
|
|
73
156
|
}
|
|
74
157
|
/**
|
|
75
158
|
* Returns a LangChain-compatible callbacks object.
|
|
@@ -77,18 +160,18 @@ class AiraCallbackHandler {
|
|
|
77
160
|
*/
|
|
78
161
|
asCallbacks() {
|
|
79
162
|
return {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
this.handleToolEnd(String(output), name);
|
|
84
|
-
},
|
|
85
|
-
handleChainEnd: (outputs) => {
|
|
86
|
-
this.handleChainEnd((outputs ?? {}));
|
|
87
|
-
},
|
|
88
|
-
handleLLMEnd: (response) => {
|
|
89
|
-
const resp = response;
|
|
90
|
-
this.handleLLMEnd(resp?.generations?.length ?? 0);
|
|
163
|
+
handleToolStart: (tool, input, runId) => this.handleToolStart(tool, String(input ?? ""), String(runId ?? "")),
|
|
164
|
+
handleToolEnd: (output, runId, ...rest) => {
|
|
165
|
+
const meta = rest[1];
|
|
166
|
+
return this.handleToolEnd(String(output), String(runId ?? ""), meta?.name ?? "unknown");
|
|
91
167
|
},
|
|
168
|
+
handleToolError: (err, runId) => this.handleToolError(err, String(runId ?? "")),
|
|
169
|
+
handleChainStart: (chain, inputs, runId) => this.handleChainStart(chain, (inputs ?? {}), String(runId ?? "")),
|
|
170
|
+
handleChainEnd: (outputs, runId) => this.handleChainEnd((outputs ?? {}), String(runId ?? "")),
|
|
171
|
+
handleChainError: (err, runId) => this.handleChainError(err, String(runId ?? "")),
|
|
172
|
+
handleLLMStart: (llm, prompts, runId) => this.handleLLMStart(llm, prompts ?? [], String(runId ?? "")),
|
|
173
|
+
handleLLMEnd: (response, runId) => this.handleLLMEnd(response, String(runId ?? "")),
|
|
174
|
+
handleLLMError: (err, runId) => this.handleLLMError(err, String(runId ?? "")),
|
|
92
175
|
};
|
|
93
176
|
}
|
|
94
177
|
}
|
package/dist/extras/mcp.d.ts
CHANGED
|
@@ -3,9 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Requires: @modelcontextprotocol/sdk (peer dependency)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* ---------------------------------------------------------------------------
|
|
7
|
+
* LIFECYCLE & DESIGN NOTES
|
|
8
|
+
* ---------------------------------------------------------------------------
|
|
9
|
+
*
|
|
10
|
+
* MCP is a bidirectional protocol: the host (an AI agent) connects to this
|
|
11
|
+
* server and calls the tools explicitly. There is no "wrap" moment — the
|
|
12
|
+
* agent *chooses* to invoke `authorize_action` before performing the side
|
|
13
|
+
* effect and `notarize_action` after. There is no hidden hook point.
|
|
14
|
+
*
|
|
15
|
+
* That makes this integration AUDIT-ONLY in the sense that we don't own the
|
|
16
|
+
* execution boundary: we can only do what the caller asks us to do. But the
|
|
17
|
+
* exposed tools faithfully implement the two-step flow, so an MCP client
|
|
18
|
+
* that follows the contract gets the full authorization gate.
|
|
19
|
+
*
|
|
20
|
+
* Exposed tools:
|
|
21
|
+
* - authorize_action → POST /api/v1/actions
|
|
22
|
+
* - notarize_action → POST /api/v1/actions/{id}/notarize
|
|
23
|
+
* - get_action → GET /api/v1/actions/{id}
|
|
24
|
+
* - verify_action → GET /api/v1/verify/action/{id}
|
|
25
|
+
* - get_receipt → GET /api/v1/receipts/{id}
|
|
26
|
+
* - resolve_did → POST /api/v1/dids/resolve
|
|
27
|
+
* - verify_credential → POST /api/v1/credentials/verify (via agent slug)
|
|
28
|
+
* - get_reputation → GET /api/v1/agents/{slug}/reputation
|
|
29
|
+
* - request_mutual_sign → POST /api/v1/actions/{id}/mutual-sign/request
|
|
9
30
|
*/
|
|
10
31
|
import type { Aira } from "../client";
|
|
11
32
|
/** Tool definition for MCP list_tools response. */
|
package/dist/extras/mcp.js
CHANGED
|
@@ -4,9 +4,30 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Requires: @modelcontextprotocol/sdk (peer dependency)
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
* LIFECYCLE & DESIGN NOTES
|
|
9
|
+
* ---------------------------------------------------------------------------
|
|
10
|
+
*
|
|
11
|
+
* MCP is a bidirectional protocol: the host (an AI agent) connects to this
|
|
12
|
+
* server and calls the tools explicitly. There is no "wrap" moment — the
|
|
13
|
+
* agent *chooses* to invoke `authorize_action` before performing the side
|
|
14
|
+
* effect and `notarize_action` after. There is no hidden hook point.
|
|
15
|
+
*
|
|
16
|
+
* That makes this integration AUDIT-ONLY in the sense that we don't own the
|
|
17
|
+
* execution boundary: we can only do what the caller asks us to do. But the
|
|
18
|
+
* exposed tools faithfully implement the two-step flow, so an MCP client
|
|
19
|
+
* that follows the contract gets the full authorization gate.
|
|
20
|
+
*
|
|
21
|
+
* Exposed tools:
|
|
22
|
+
* - authorize_action → POST /api/v1/actions
|
|
23
|
+
* - notarize_action → POST /api/v1/actions/{id}/notarize
|
|
24
|
+
* - get_action → GET /api/v1/actions/{id}
|
|
25
|
+
* - verify_action → GET /api/v1/verify/action/{id}
|
|
26
|
+
* - get_receipt → GET /api/v1/receipts/{id}
|
|
27
|
+
* - resolve_did → POST /api/v1/dids/resolve
|
|
28
|
+
* - verify_credential → POST /api/v1/credentials/verify (via agent slug)
|
|
29
|
+
* - get_reputation → GET /api/v1/agents/{slug}/reputation
|
|
30
|
+
* - request_mutual_sign → POST /api/v1/actions/{id}/mutual-sign/request
|
|
10
31
|
*/
|
|
11
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
33
|
exports.getTools = getTools;
|
|
@@ -17,19 +38,45 @@ const types_1 = require("../types");
|
|
|
17
38
|
function getTools() {
|
|
18
39
|
return [
|
|
19
40
|
{
|
|
20
|
-
name: "
|
|
21
|
-
description: "
|
|
41
|
+
name: "authorize_action",
|
|
42
|
+
description: "Step 1 of the Aira two-step flow. Authorize an action BEFORE it executes. Returns an action_id with status 'authorized' or 'pending_approval'. Throws POLICY_DENIED if a policy blocks the action.",
|
|
22
43
|
inputSchema: {
|
|
23
44
|
type: "object",
|
|
24
45
|
properties: {
|
|
25
|
-
action_type: { type: "string", description: "e.g. email_sent, loan_approved,
|
|
26
|
-
details: { type: "string", description: "What
|
|
46
|
+
action_type: { type: "string", description: "e.g. email_sent, loan_approved, wire_transfer" },
|
|
47
|
+
details: { type: "string", description: "What the agent is about to do" },
|
|
27
48
|
agent_id: { type: "string", description: "Agent slug" },
|
|
28
49
|
model_id: { type: "string", description: "Model used (optional)" },
|
|
50
|
+
require_approval: { type: "boolean", description: "Force human approval (optional)" },
|
|
51
|
+
approvers: { type: "array", items: { type: "string" }, description: "Approver emails (optional)" },
|
|
29
52
|
},
|
|
30
53
|
required: ["action_type", "details"],
|
|
31
54
|
},
|
|
32
55
|
},
|
|
56
|
+
{
|
|
57
|
+
name: "notarize_action",
|
|
58
|
+
description: "Step 2 of the Aira two-step flow. Notarize the outcome of an already-authorized action. Call this AFTER the action has been executed. Returns a cryptographic receipt when outcome is 'completed'.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
action_id: { type: "string", description: "action_id returned from authorize_action" },
|
|
63
|
+
outcome: { type: "string", enum: ["completed", "failed"], description: "Did the action succeed?" },
|
|
64
|
+
outcome_details: { type: "string", description: "Optional description of the outcome" },
|
|
65
|
+
},
|
|
66
|
+
required: ["action_id"],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "get_action",
|
|
71
|
+
description: "Retrieve full details of an action including its receipt and authorizations",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
action_id: { type: "string", description: "Action UUID" },
|
|
76
|
+
},
|
|
77
|
+
required: ["action_id"],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
33
80
|
{
|
|
34
81
|
name: "verify_action",
|
|
35
82
|
description: "Verify a notarized action's cryptographic receipt",
|
|
@@ -102,15 +149,29 @@ function getTools() {
|
|
|
102
149
|
/** Handle an MCP tool call and return text content. */
|
|
103
150
|
async function handleToolCall(client, name, args) {
|
|
104
151
|
try {
|
|
105
|
-
if (name === "
|
|
106
|
-
const result = await client.
|
|
152
|
+
if (name === "authorize_action") {
|
|
153
|
+
const result = await client.authorize({
|
|
107
154
|
actionType: args.action_type,
|
|
108
155
|
details: args.details,
|
|
109
156
|
agentId: args.agent_id,
|
|
110
157
|
modelId: args.model_id,
|
|
158
|
+
requireApproval: args.require_approval,
|
|
159
|
+
approvers: args.approvers,
|
|
160
|
+
});
|
|
161
|
+
return [{ type: "text", text: JSON.stringify(result) }];
|
|
162
|
+
}
|
|
163
|
+
if (name === "notarize_action") {
|
|
164
|
+
const result = await client.notarize({
|
|
165
|
+
actionId: args.action_id,
|
|
166
|
+
outcome: args.outcome ?? "completed",
|
|
167
|
+
outcomeDetails: args.outcome_details,
|
|
111
168
|
});
|
|
112
169
|
return [{ type: "text", text: JSON.stringify(result) }];
|
|
113
170
|
}
|
|
171
|
+
if (name === "get_action") {
|
|
172
|
+
const result = await client.getAction(args.action_id);
|
|
173
|
+
return [{ type: "text", text: JSON.stringify(result) }];
|
|
174
|
+
}
|
|
114
175
|
if (name === "verify_action") {
|
|
115
176
|
const result = await client.verifyAction(args.action_id);
|
|
116
177
|
return [{ type: "text", text: JSON.stringify(result) }];
|