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/README.md +300 -104
- package/dist/client.d.ts +112 -4
- package/dist/client.js +152 -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/session.d.ts +15 -4
- package/dist/session.js +11 -3
- package/dist/types.d.ts +73 -20
- package/dist/types.js +15 -4
- package/package.json +1 -1
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) }];
|
|
@@ -1,37 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenAI
|
|
2
|
+
* OpenAI Agents SDK integration — pre-execution gate via tool wrapping.
|
|
3
3
|
*
|
|
4
|
-
* Requires: openai (peer dependency)
|
|
4
|
+
* Requires: @openai/agents (peer dependency)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* ---------------------------------------------------------------------------
|
|
7
|
+
* LIFECYCLE & DESIGN NOTES
|
|
8
|
+
* ---------------------------------------------------------------------------
|
|
9
|
+
*
|
|
10
|
+
* The OpenAI Agents SDK supports guardrails that run BEFORE the model produces
|
|
11
|
+
* output (`inputGuardrails`) and BEFORE a tool executes (via wrapping the
|
|
12
|
+
* tool's `execute` / function). Either path can throw to abort the run, so
|
|
13
|
+
* both qualify as a REAL authorization gate.
|
|
14
|
+
*
|
|
15
|
+
* `AiraGuardrail.wrapTool()` is the cleanest integration: it calls
|
|
16
|
+
* `aira.authorize()` before the tool runs. If the backend responds with:
|
|
17
|
+
*
|
|
18
|
+
* - "authorized" → the tool runs; `aira.notarize()` is called
|
|
19
|
+
* with outcome="completed" (or "failed" on throw).
|
|
20
|
+
* - "pending_approval" → we throw. The agent never sees the tool result;
|
|
21
|
+
* it handles the error like any other tool failure.
|
|
22
|
+
* - AiraError POLICY_DENIED → rethrown. Tool is blocked entirely.
|
|
23
|
+
*
|
|
24
|
+
* Behavior on authorize network/5xx errors is controlled by `strict`:
|
|
25
|
+
* - strict=false (default) → fail open with a warning. Tool runs, no receipt.
|
|
26
|
+
* - strict=true → fail closed. Tool throws.
|
|
10
27
|
*/
|
|
11
28
|
import type { Aira } from "../client";
|
|
12
29
|
import type { TrustPolicy, TrustContext } from "./trust";
|
|
13
30
|
export type { TrustPolicy, TrustContext } from "./trust";
|
|
31
|
+
export interface AiraGuardrailOptions {
|
|
32
|
+
modelId?: string;
|
|
33
|
+
trustPolicy?: TrustPolicy;
|
|
34
|
+
/** Fail closed if authorize() fails (network, 5xx). Default: false. */
|
|
35
|
+
strict?: boolean;
|
|
36
|
+
}
|
|
14
37
|
export declare class AiraGuardrail {
|
|
15
38
|
private client;
|
|
16
39
|
private agentId;
|
|
17
40
|
private modelId?;
|
|
18
41
|
private trustPolicy?;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
trustPolicy?: TrustPolicy;
|
|
22
|
-
});
|
|
42
|
+
private strict;
|
|
43
|
+
constructor(client: Aira, agentId: string, options?: AiraGuardrailOptions);
|
|
23
44
|
/**
|
|
24
45
|
* Check trust for a counterparty agent before interacting.
|
|
25
46
|
* Advisory by default — only blocks on revoked VC or unregistered agent if configured.
|
|
26
47
|
*/
|
|
27
48
|
checkTrust(counterpartyId: string): Promise<TrustContext>;
|
|
28
|
-
private notarize;
|
|
29
|
-
/** Call after a tool execution to notarize it. */
|
|
30
|
-
onToolCall(toolName: string, args?: Record<string, unknown>): void;
|
|
31
|
-
/** Call after a tool returns to notarize the result. */
|
|
32
|
-
onToolResult(toolName: string, result?: unknown): void;
|
|
33
49
|
/**
|
|
34
|
-
*
|
|
50
|
+
* REAL GATE: call `authorize()` for a tool invocation.
|
|
51
|
+
*
|
|
52
|
+
* Returns the action_id on success, throws on POLICY_DENIED or
|
|
53
|
+
* pending_approval. Arg keys are logged (not values) to avoid leaking
|
|
54
|
+
* sensitive user input into audit trails.
|
|
55
|
+
*/
|
|
56
|
+
authorizeToolCall(toolName: string, args?: Record<string, unknown>): Promise<string | null>;
|
|
57
|
+
/** Notarize the outcome of a previously authorized tool call. */
|
|
58
|
+
notarizeToolResult(actionId: string, toolName: string, outcome: "completed" | "failed", detail: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* REAL GATE: wraps a tool function to gate + notarize.
|
|
61
|
+
*
|
|
62
|
+
* Flow:
|
|
63
|
+
* 1. Call `aira.authorize()` — throws POLICY_DENIED or pending_approval.
|
|
64
|
+
* 2. Run the tool.
|
|
65
|
+
* 3. Call `aira.notarize()` with outcome="completed" or "failed".
|
|
66
|
+
*
|
|
35
67
|
* No raw user data is sent — only tool name, arg keys, and output length.
|
|
36
68
|
*/
|
|
37
69
|
wrapTool<T extends (...args: unknown[]) => unknown>(toolFn: T, toolName?: string): T;
|
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* OpenAI
|
|
3
|
+
* OpenAI Agents SDK integration — pre-execution gate via tool wrapping.
|
|
4
4
|
*
|
|
5
|
-
* Requires: openai (peer dependency)
|
|
5
|
+
* Requires: @openai/agents (peer dependency)
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
* LIFECYCLE & DESIGN NOTES
|
|
9
|
+
* ---------------------------------------------------------------------------
|
|
10
|
+
*
|
|
11
|
+
* The OpenAI Agents SDK supports guardrails that run BEFORE the model produces
|
|
12
|
+
* output (`inputGuardrails`) and BEFORE a tool executes (via wrapping the
|
|
13
|
+
* tool's `execute` / function). Either path can throw to abort the run, so
|
|
14
|
+
* both qualify as a REAL authorization gate.
|
|
15
|
+
*
|
|
16
|
+
* `AiraGuardrail.wrapTool()` is the cleanest integration: it calls
|
|
17
|
+
* `aira.authorize()` before the tool runs. If the backend responds with:
|
|
18
|
+
*
|
|
19
|
+
* - "authorized" → the tool runs; `aira.notarize()` is called
|
|
20
|
+
* with outcome="completed" (or "failed" on throw).
|
|
21
|
+
* - "pending_approval" → we throw. The agent never sees the tool result;
|
|
22
|
+
* it handles the error like any other tool failure.
|
|
23
|
+
* - AiraError POLICY_DENIED → rethrown. Tool is blocked entirely.
|
|
24
|
+
*
|
|
25
|
+
* Behavior on authorize network/5xx errors is controlled by `strict`:
|
|
26
|
+
* - strict=false (default) → fail open with a warning. Tool runs, no receipt.
|
|
27
|
+
* - strict=true → fail closed. Tool throws.
|
|
11
28
|
*/
|
|
12
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
30
|
exports.AiraGuardrail = void 0;
|
|
@@ -18,11 +35,13 @@ class AiraGuardrail {
|
|
|
18
35
|
agentId;
|
|
19
36
|
modelId;
|
|
20
37
|
trustPolicy;
|
|
38
|
+
strict;
|
|
21
39
|
constructor(client, agentId, options) {
|
|
22
40
|
this.client = client;
|
|
23
41
|
this.agentId = agentId;
|
|
24
42
|
this.modelId = options?.modelId;
|
|
25
43
|
this.trustPolicy = options?.trustPolicy;
|
|
44
|
+
this.strict = options?.strict ?? false;
|
|
26
45
|
}
|
|
27
46
|
/**
|
|
28
47
|
* Check trust for a counterparty agent before interacting.
|
|
@@ -34,34 +53,61 @@ class AiraGuardrail {
|
|
|
34
53
|
}
|
|
35
54
|
return (0, trust_1.checkTrust)(this.client, this.trustPolicy, counterpartyId);
|
|
36
55
|
}
|
|
37
|
-
|
|
56
|
+
/**
|
|
57
|
+
* REAL GATE: call `authorize()` for a tool invocation.
|
|
58
|
+
*
|
|
59
|
+
* Returns the action_id on success, throws on POLICY_DENIED or
|
|
60
|
+
* pending_approval. Arg keys are logged (not values) to avoid leaking
|
|
61
|
+
* sensitive user input into audit trails.
|
|
62
|
+
*/
|
|
63
|
+
async authorizeToolCall(toolName, args) {
|
|
64
|
+
const argKeys = Object.keys(args ?? {});
|
|
38
65
|
try {
|
|
39
|
-
const
|
|
40
|
-
actionType,
|
|
41
|
-
details:
|
|
66
|
+
const auth = await this.client.authorize({
|
|
67
|
+
actionType: "tool_call",
|
|
68
|
+
details: `Tool '${toolName}' called. Arg keys: [${argKeys.join(", ")}]`.slice(0, MAX_DETAILS),
|
|
42
69
|
agentId: this.agentId,
|
|
43
|
-
|
|
44
|
-
if (this.modelId)
|
|
45
|
-
params.modelId = this.modelId;
|
|
46
|
-
this.client.notarize(params).catch((e) => {
|
|
47
|
-
console.warn("Aira notarize failed (non-blocking):", e);
|
|
70
|
+
modelId: this.modelId,
|
|
48
71
|
});
|
|
72
|
+
if (auth.status === "pending_approval") {
|
|
73
|
+
const err = new Error(`Aira: tool '${toolName}' is pending human approval (action_id=${auth.action_id}). Tool execution blocked.`);
|
|
74
|
+
err.code = "PENDING_APPROVAL";
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
return auth.action_id;
|
|
49
78
|
}
|
|
50
79
|
catch (e) {
|
|
51
|
-
|
|
80
|
+
const err = e;
|
|
81
|
+
// Always propagate authorization-layer rejections.
|
|
82
|
+
if (err.code === "POLICY_DENIED" || err.code === "PENDING_APPROVAL")
|
|
83
|
+
throw e;
|
|
84
|
+
if (this.strict)
|
|
85
|
+
throw e;
|
|
86
|
+
console.warn("Aira authorize failed (fail-open):", err);
|
|
87
|
+
return null;
|
|
52
88
|
}
|
|
53
89
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
90
|
+
/** Notarize the outcome of a previously authorized tool call. */
|
|
91
|
+
async notarizeToolResult(actionId, toolName, outcome, detail) {
|
|
92
|
+
try {
|
|
93
|
+
await this.client.notarize({
|
|
94
|
+
actionId,
|
|
95
|
+
outcome,
|
|
96
|
+
outcomeDetails: `Tool '${toolName}' ${outcome}: ${detail}`.slice(0, MAX_DETAILS),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
console.warn("Aira notarize failed (non-blocking):", e);
|
|
101
|
+
}
|
|
62
102
|
}
|
|
63
103
|
/**
|
|
64
|
-
*
|
|
104
|
+
* REAL GATE: wraps a tool function to gate + notarize.
|
|
105
|
+
*
|
|
106
|
+
* Flow:
|
|
107
|
+
* 1. Call `aira.authorize()` — throws POLICY_DENIED or pending_approval.
|
|
108
|
+
* 2. Run the tool.
|
|
109
|
+
* 3. Call `aira.notarize()` with outcome="completed" or "failed".
|
|
110
|
+
*
|
|
65
111
|
* No raw user data is sent — only tool name, arg keys, and output length.
|
|
66
112
|
*/
|
|
67
113
|
wrapTool(toolFn, toolName) {
|
|
@@ -71,10 +117,20 @@ class AiraGuardrail {
|
|
|
71
117
|
const kwargs = args.length > 0 && typeof args[0] === "object" && args[0]
|
|
72
118
|
? args[0]
|
|
73
119
|
: undefined;
|
|
74
|
-
self.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
const actionId = await self.authorizeToolCall(name, kwargs);
|
|
121
|
+
try {
|
|
122
|
+
const result = await toolFn.apply(this, args);
|
|
123
|
+
if (actionId) {
|
|
124
|
+
await self.notarizeToolResult(actionId, name, "completed", `result length ${String(result).length} chars`);
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (actionId) {
|
|
130
|
+
await self.notarizeToolResult(actionId, name, "failed", err?.message ?? String(err));
|
|
131
|
+
}
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
78
134
|
};
|
|
79
135
|
return wrapped;
|
|
80
136
|
}
|