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.
@@ -1,13 +1,39 @@
1
1
  "use strict";
2
2
  /**
3
- * LangChain.js integration — auto-notarize tool and chain completions.
3
+ * LangChain.js integration — pre-execution gate + post-execution notarize.
4
4
  *
5
5
  * Requires: @langchain/core (peer dependency)
6
6
  *
7
- * Usage:
8
- * import { AiraCallbackHandler } from "aira-sdk/extras/langchain";
9
- * const handler = new AiraCallbackHandler(aira, "my-agent");
10
- * const chain = someChain.withConfig({ callbacks: [handler] });
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
- tool_end: "tool_call",
29
- chain_end: "chain_completed",
30
- llm_end: "llm_completion",
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
- notarize(actionType, details) {
74
+ async doAuthorize(actionType, details, runId) {
45
75
  try {
46
- const params = {
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
- if (this.modelId)
52
- params.modelId = this.modelId;
53
- this.client.notarize(params).catch((e) => {
54
- console.warn("Aira notarize failed (non-blocking):", e);
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 when a tool finishes. */
62
- handleToolEnd(output, name = "unknown") {
63
- this.notarize(this.actionTypes.tool_end, `Tool '${name}' completed. Output length: ${String(output).length} chars`);
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 when a chain finishes. */
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.notarize(this.actionTypes.chain_end, `Chain completed. Output keys: [${keys.join(", ")}]`);
138
+ await this.doNotarize(runId, "completed", `Chain completed. Output keys: [${keys.join(", ")}]`);
69
139
  }
70
- /** Called when an LLM finishes. */
71
- handleLLMEnd(generationCount) {
72
- this.notarize(this.actionTypes.llm_end, `LLM completed. Generations: ${generationCount}`);
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
- handleToolEnd: (output, ...args) => {
81
- const runId = args[1];
82
- const name = args[2]?.name ?? "unknown";
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
  }
@@ -3,9 +3,30 @@
3
3
  *
4
4
  * Requires: @modelcontextprotocol/sdk (peer dependency)
5
5
  *
6
- * Usage:
7
- * import { createServer } from "aira-sdk/extras/mcp";
8
- * const server = createServer({ apiKey: "aira_live_xxx" });
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. */
@@ -4,9 +4,30 @@
4
4
  *
5
5
  * Requires: @modelcontextprotocol/sdk (peer dependency)
6
6
  *
7
- * Usage:
8
- * import { createServer } from "aira-sdk/extras/mcp";
9
- * const server = createServer({ apiKey: "aira_live_xxx" });
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: "notarize_action",
21
- description: "Notarize an AI agent action with a cryptographic receipt",
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, claim_processed" },
26
- details: { type: "string", description: "What happened" },
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 === "notarize_action") {
106
- const result = await client.notarize({
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 Node SDK integration — guardrail that notarizes tool calls.
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
- * Usage:
7
- * import { AiraGuardrail } from "aira-sdk/extras/openai-agents";
8
- * const guardrail = new AiraGuardrail(aira, "my-agent");
9
- * guardrail.onToolCall("search", { query: "test" });
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
- constructor(client: Aira, agentId: string, options?: {
20
- modelId?: string;
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
- * Wraps a tool function to auto-notarize calls and results.
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 Node SDK integration — guardrail that notarizes tool calls.
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
- * Usage:
8
- * import { AiraGuardrail } from "aira-sdk/extras/openai-agents";
9
- * const guardrail = new AiraGuardrail(aira, "my-agent");
10
- * guardrail.onToolCall("search", { query: "test" });
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
- notarize(actionType, details) {
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 params = {
40
- actionType,
41
- details: details.slice(0, MAX_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
- console.warn("Aira notarize failed (non-blocking):", e);
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
- /** Call after a tool execution to notarize it. */
55
- onToolCall(toolName, args) {
56
- const argKeys = Object.keys(args ?? {});
57
- this.notarize("tool_call", `Tool '${toolName}' called. Arg keys: [${argKeys.join(", ")}]`);
58
- }
59
- /** Call after a tool returns to notarize the result. */
60
- onToolResult(toolName, result) {
61
- this.notarize("tool_completed", `Tool '${toolName}' completed. Result length: ${String(result).length} chars`);
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
- * Wraps a tool function to auto-notarize calls and results.
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.onToolCall(name, kwargs);
75
- const result = await toolFn.apply(this, args);
76
- self.onToolResult(name, result);
77
- return result;
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
  }