@armoriq/sdk-dev 0.0.1
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/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/client.d.ts +135 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +858 -0
- package/dist/client.js.map +1 -0
- package/dist/exceptions.d.ts +101 -0
- package/dist/exceptions.d.ts.map +1 -0
- package/dist/exceptions.js +164 -0
- package/dist/exceptions.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +265 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +27 -0
- package/dist/models.js.map +1 -0
- package/package.json +57 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ArmorIQ SDK Client - Main entry point for SDK usage.
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.ArmorIQClient = void 0;
|
|
43
|
+
const axios_1 = __importDefault(require("axios"));
|
|
44
|
+
const crypto = __importStar(require("crypto"));
|
|
45
|
+
const models_1 = require("./models");
|
|
46
|
+
const exceptions_1 = require("./exceptions");
|
|
47
|
+
/**
|
|
48
|
+
* Main client for ArmorIQ SDK.
|
|
49
|
+
*
|
|
50
|
+
* Provides high-level APIs for:
|
|
51
|
+
* - Plan capture and canonicalization
|
|
52
|
+
* - Intent token management
|
|
53
|
+
* - MCP action invocation
|
|
54
|
+
* - Agent delegation
|
|
55
|
+
*/
|
|
56
|
+
class ArmorIQClient {
|
|
57
|
+
// Production endpoints (default) - ArmorIQ platform
|
|
58
|
+
static DEFAULT_IAP_ENDPOINT = 'https://iap.armoriq.io';
|
|
59
|
+
static DEFAULT_PROXY_ENDPOINT = 'https://cloud-run-proxy.armoriq.io';
|
|
60
|
+
static DEFAULT_BACKEND_ENDPOINT = 'https://staging-api.armoriq.io';
|
|
61
|
+
// ArmorClaw standalone product endpoints
|
|
62
|
+
static ARMORCLAW_IAP_ENDPOINT = 'https://iap.armorclaw.io';
|
|
63
|
+
static ARMORCLAW_PROXY_ENDPOINT = 'https://proxy.armorclaw.io';
|
|
64
|
+
static ARMORCLAW_BACKEND_ENDPOINT = 'https://api.armorclaw.io';
|
|
65
|
+
// Local development endpoints - ArmorIQ platform
|
|
66
|
+
static LOCAL_IAP_ENDPOINT = 'http://127.0.0.1:8000';
|
|
67
|
+
static LOCAL_PROXY_ENDPOINT = 'http://127.0.0.1:3001';
|
|
68
|
+
static LOCAL_BACKEND_ENDPOINT = 'http://127.0.0.1:3000';
|
|
69
|
+
// Local development endpoints - ArmorClaw standalone
|
|
70
|
+
static LOCAL_ARMORCLAW_IAP_ENDPOINT = 'http://127.0.0.1:8080';
|
|
71
|
+
static LOCAL_ARMORCLAW_PROXY_ENDPOINT = 'http://127.0.0.1:3001';
|
|
72
|
+
static LOCAL_ARMORCLAW_BACKEND_ENDPOINT = 'http://127.0.0.1:8081';
|
|
73
|
+
iapEndpoint;
|
|
74
|
+
defaultProxyEndpoint;
|
|
75
|
+
backendEndpoint;
|
|
76
|
+
proxyEndpoints;
|
|
77
|
+
userId;
|
|
78
|
+
agentId;
|
|
79
|
+
contextId;
|
|
80
|
+
apiKey;
|
|
81
|
+
timeout;
|
|
82
|
+
maxRetries;
|
|
83
|
+
verifySsl;
|
|
84
|
+
httpClient;
|
|
85
|
+
tokenCache;
|
|
86
|
+
metadataCache;
|
|
87
|
+
constructor(options = {}) {
|
|
88
|
+
// Determine if using production based on environment
|
|
89
|
+
const envMode = process.env.ARMORIQ_ENV?.toLowerCase() || 'production';
|
|
90
|
+
const useProd = (options.useProduction ?? true) && envMode === 'production';
|
|
91
|
+
// Resolve API key early to determine product routing
|
|
92
|
+
const resolvedApiKey = options.apiKey || process.env.ARMORIQ_API_KEY || '';
|
|
93
|
+
const isArmorClaw = resolvedApiKey.startsWith('ak_claw_');
|
|
94
|
+
// Route endpoints based on API key prefix (Stripe pattern)
|
|
95
|
+
// ak_claw_ → ArmorClaw standalone, ak_live_ → ArmorIQ platform, ak_test_ → sandbox
|
|
96
|
+
this.iapEndpoint =
|
|
97
|
+
options.iapEndpoint ||
|
|
98
|
+
process.env.IAP_ENDPOINT ||
|
|
99
|
+
(isArmorClaw
|
|
100
|
+
? (useProd ? ArmorIQClient.ARMORCLAW_IAP_ENDPOINT : ArmorIQClient.LOCAL_ARMORCLAW_IAP_ENDPOINT)
|
|
101
|
+
: (useProd ? ArmorIQClient.DEFAULT_IAP_ENDPOINT : ArmorIQClient.LOCAL_IAP_ENDPOINT));
|
|
102
|
+
this.defaultProxyEndpoint =
|
|
103
|
+
options.proxyEndpoint ||
|
|
104
|
+
process.env.PROXY_ENDPOINT ||
|
|
105
|
+
(isArmorClaw
|
|
106
|
+
? (useProd ? ArmorIQClient.ARMORCLAW_PROXY_ENDPOINT : ArmorIQClient.LOCAL_ARMORCLAW_PROXY_ENDPOINT)
|
|
107
|
+
: (useProd ? ArmorIQClient.DEFAULT_PROXY_ENDPOINT : ArmorIQClient.LOCAL_PROXY_ENDPOINT));
|
|
108
|
+
this.backendEndpoint =
|
|
109
|
+
options.backendEndpoint ||
|
|
110
|
+
process.env.BACKEND_ENDPOINT ||
|
|
111
|
+
(isArmorClaw
|
|
112
|
+
? (useProd ? ArmorIQClient.ARMORCLAW_BACKEND_ENDPOINT : ArmorIQClient.LOCAL_ARMORCLAW_BACKEND_ENDPOINT)
|
|
113
|
+
: (useProd ? ArmorIQClient.DEFAULT_BACKEND_ENDPOINT : ArmorIQClient.LOCAL_BACKEND_ENDPOINT));
|
|
114
|
+
// Load user/agent identifiers
|
|
115
|
+
this.userId = options.userId || process.env.USER_ID || '';
|
|
116
|
+
this.agentId = options.agentId || process.env.AGENT_ID || '';
|
|
117
|
+
this.contextId = options.contextId || process.env.CONTEXT_ID || 'default';
|
|
118
|
+
this.apiKey = options.apiKey || process.env.ARMORIQ_API_KEY || '';
|
|
119
|
+
// Validate required config
|
|
120
|
+
if (!this.apiKey) {
|
|
121
|
+
throw new exceptions_1.ConfigurationException('API key is required for Customer SDK. ' +
|
|
122
|
+
'Set ARMORIQ_API_KEY environment variable or pass apiKey parameter. ' +
|
|
123
|
+
'Get your API key from https://platform.armoriq.ai/dashboard/api-keys');
|
|
124
|
+
}
|
|
125
|
+
// Validate API key format
|
|
126
|
+
if (!this.apiKey.startsWith('ak_live_') && !this.apiKey.startsWith('ak_test_') && !this.apiKey.startsWith('ak_claw_')) {
|
|
127
|
+
throw new exceptions_1.ConfigurationException("Invalid API key format. API keys must start with 'ak_live_', 'ak_claw_', or 'ak_test_'. " +
|
|
128
|
+
'Get your API key from https://platform.armoriq.ai/dashboard/api-keys');
|
|
129
|
+
}
|
|
130
|
+
if (!this.userId) {
|
|
131
|
+
throw new exceptions_1.ConfigurationException('userId is required (set USER_ID env var)');
|
|
132
|
+
}
|
|
133
|
+
if (!this.agentId) {
|
|
134
|
+
throw new exceptions_1.ConfigurationException('agentId is required (set AGENT_ID env var)');
|
|
135
|
+
}
|
|
136
|
+
this.proxyEndpoints = options.proxyEndpoints || {};
|
|
137
|
+
this.timeout = options.timeout || 30000;
|
|
138
|
+
this.maxRetries = options.maxRetries || 3;
|
|
139
|
+
this.verifySsl = options.verifySsl ?? true;
|
|
140
|
+
// Initialize HTTP client
|
|
141
|
+
const headers = {
|
|
142
|
+
'User-Agent': `ArmorIQ-SDK-TS/0.2.6 (agent=${this.agentId})`,
|
|
143
|
+
};
|
|
144
|
+
if (this.apiKey) {
|
|
145
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
146
|
+
}
|
|
147
|
+
this.httpClient = axios_1.default.create({
|
|
148
|
+
timeout: this.timeout,
|
|
149
|
+
headers,
|
|
150
|
+
validateStatus: () => true, // Handle all status codes manually
|
|
151
|
+
});
|
|
152
|
+
this.tokenCache = new Map();
|
|
153
|
+
this.metadataCache = new Map();
|
|
154
|
+
console.log(`ArmorIQ SDK initialized: mode=${useProd ? 'production' : 'development'}, ` +
|
|
155
|
+
`user=${this.userId}, agent=${this.agentId}, ` +
|
|
156
|
+
`iap=${this.iapEndpoint}, proxy=${this.defaultProxyEndpoint}, ` +
|
|
157
|
+
`backend=${this.backendEndpoint}, ` +
|
|
158
|
+
`api_key=${'***' + this.apiKey.slice(-8)}`);
|
|
159
|
+
// Validate API key on initialization
|
|
160
|
+
this.validateApiKey();
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get the default proxy endpoint URL.
|
|
164
|
+
*/
|
|
165
|
+
get proxyEndpoint() {
|
|
166
|
+
return this.defaultProxyEndpoint;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Validate API key with the proxy server.
|
|
170
|
+
*/
|
|
171
|
+
async validateApiKey() {
|
|
172
|
+
try {
|
|
173
|
+
const response = await this.httpClient.get(`${this.proxyEndpoint}/health`, {
|
|
174
|
+
headers: { 'X-API-Key': this.apiKey },
|
|
175
|
+
timeout: 5000,
|
|
176
|
+
});
|
|
177
|
+
if (response.status === 401) {
|
|
178
|
+
throw new exceptions_1.ConfigurationException('Invalid API key. Please check your API key at https://platform.armoriq.ai/dashboard/api-keys');
|
|
179
|
+
}
|
|
180
|
+
else if (response.status >= 400) {
|
|
181
|
+
console.warn(`API key validation returned status ${response.status}, but continuing...`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log('✅ API key validated successfully');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (error instanceof exceptions_1.ConfigurationException) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
|
|
192
|
+
console.warn(`Could not connect to proxy at ${this.proxyEndpoint} for API key validation`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
console.warn(`API key validation check failed: ${error.message}, but continuing...`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Capture an execution plan structure.
|
|
201
|
+
*/
|
|
202
|
+
capturePlan(llm, prompt, plan, metadata) {
|
|
203
|
+
console.log(`Capturing plan: llm=${llm}, prompt=${prompt.slice(0, 50)}...`);
|
|
204
|
+
if (!plan) {
|
|
205
|
+
throw new Error('Plan structure is required. ' +
|
|
206
|
+
'You must provide an explicit plan with the MCP and actions you want to execute.\n\n' +
|
|
207
|
+
'Example:\n' +
|
|
208
|
+
' const plan = client.capturePlan(\n' +
|
|
209
|
+
" 'gpt-4',\n" +
|
|
210
|
+
" 'Your task description',\n" +
|
|
211
|
+
' {\n' +
|
|
212
|
+
" goal: 'Your task description',\n" +
|
|
213
|
+
' steps: [\n' +
|
|
214
|
+
' {\n' +
|
|
215
|
+
" action: 'your_tool_name',\n" +
|
|
216
|
+
" mcp: 'your-mcp-name',\n" +
|
|
217
|
+
" params: { param1: 'value1' }\n" +
|
|
218
|
+
' }\n' +
|
|
219
|
+
' ]\n' +
|
|
220
|
+
' }\n' +
|
|
221
|
+
' );\n\n' +
|
|
222
|
+
'Note: Use the MCP name and tool names from your onboarded MCPs on the ArmorIQ platform.');
|
|
223
|
+
}
|
|
224
|
+
if (!plan.steps) {
|
|
225
|
+
throw new Error("Plan must contain 'steps' key");
|
|
226
|
+
}
|
|
227
|
+
const capture = {
|
|
228
|
+
plan,
|
|
229
|
+
llm,
|
|
230
|
+
prompt,
|
|
231
|
+
metadata: metadata || {},
|
|
232
|
+
};
|
|
233
|
+
console.log(`Plan captured with ${plan.steps?.length || 0} steps`);
|
|
234
|
+
return capture;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Request a signed intent token from IAP for the given plan.
|
|
238
|
+
*/
|
|
239
|
+
async getIntentToken(planCapture, policy, validitySeconds = 60.0) {
|
|
240
|
+
console.log(`Requesting intent token for plan with ${planCapture.plan.steps?.length || 0} steps`);
|
|
241
|
+
const payload = {
|
|
242
|
+
user_id: this.userId,
|
|
243
|
+
agent_id: this.agentId,
|
|
244
|
+
context_id: this.contextId,
|
|
245
|
+
plan: planCapture.plan,
|
|
246
|
+
policy,
|
|
247
|
+
expires_in: validitySeconds,
|
|
248
|
+
};
|
|
249
|
+
try {
|
|
250
|
+
const response = await this.httpClient.post(`${this.backendEndpoint}/iap/sdk/token`, payload, {
|
|
251
|
+
headers: { 'X-API-Key': this.apiKey },
|
|
252
|
+
timeout: 30000,
|
|
253
|
+
});
|
|
254
|
+
if (response.status >= 400) {
|
|
255
|
+
throw new exceptions_1.InvalidTokenException(`Token issuance failed: ${response.data?.message || response.statusText}`);
|
|
256
|
+
}
|
|
257
|
+
const data = response.data;
|
|
258
|
+
if (!data.success) {
|
|
259
|
+
throw new exceptions_1.InvalidTokenException(`Token issuance failed: ${data.message || 'Unknown error'}`);
|
|
260
|
+
}
|
|
261
|
+
const tokenData = data.token || {};
|
|
262
|
+
const rawToken = {
|
|
263
|
+
plan: planCapture.plan,
|
|
264
|
+
plan_id: data.plan_id,
|
|
265
|
+
token: tokenData,
|
|
266
|
+
plan_hash: data.plan_hash,
|
|
267
|
+
merkle_root: data.merkle_root,
|
|
268
|
+
intent_reference: data.intent_reference,
|
|
269
|
+
composite_identity: data.composite_identity || '',
|
|
270
|
+
step_proofs: data.step_proofs || [],
|
|
271
|
+
};
|
|
272
|
+
const token = {
|
|
273
|
+
tokenId: data.intent_reference || 'unknown',
|
|
274
|
+
planHash: data.plan_hash || '',
|
|
275
|
+
planId: data.plan_id,
|
|
276
|
+
signature: typeof tokenData === 'object' ? tokenData.signature || '' : '',
|
|
277
|
+
issuedAt: Date.now() / 1000,
|
|
278
|
+
expiresAt: Date.now() / 1000 + validitySeconds,
|
|
279
|
+
policy: policy || {},
|
|
280
|
+
compositeIdentity: data.composite_identity || '',
|
|
281
|
+
clientInfo: data.client_info,
|
|
282
|
+
policyValidation: data.policy_validation,
|
|
283
|
+
stepProofs: data.step_proofs || [],
|
|
284
|
+
totalSteps: planCapture.plan.steps?.length || 0,
|
|
285
|
+
rawToken,
|
|
286
|
+
jwtToken: data.jwt_token,
|
|
287
|
+
policySnapshot: data.policy_snapshot,
|
|
288
|
+
};
|
|
289
|
+
console.log(`Intent token issued: id=${token.tokenId}, plan_hash=${token.planHash.slice(0, 16)}..., ` +
|
|
290
|
+
`expires=${models_1.IntentToken.timeUntilExpiry(token).toFixed(1)}s, stepProofs=${token.stepProofs?.length || 0}`);
|
|
291
|
+
return token;
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
const responseData = error?.response?.data;
|
|
295
|
+
const deniedTools = responseData?.policy_validation?.denied_tools;
|
|
296
|
+
const deniedReasons = responseData?.policy_validation?.denied_reasons;
|
|
297
|
+
if (error?.response?.status === 403 || (Array.isArray(deniedTools) && deniedTools.length > 0)) {
|
|
298
|
+
const reason = (Array.isArray(deniedReasons) && deniedReasons.length > 0
|
|
299
|
+
? deniedReasons.join('; ')
|
|
300
|
+
: responseData?.message) || 'Blocked by policy';
|
|
301
|
+
throw new exceptions_1.PolicyBlockedException(`Policy blocked intent token issuance: ${reason}`, responseData?.policy_validation?.default_enforcement_action, reason, responseData?.policy_validation);
|
|
302
|
+
}
|
|
303
|
+
if (error instanceof exceptions_1.InvalidTokenException) {
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
const message = responseData?.message ||
|
|
307
|
+
(typeof responseData === 'string' ? responseData : undefined) ||
|
|
308
|
+
error.message;
|
|
309
|
+
throw new exceptions_1.InvalidTokenException(`Failed to get intent token: ${message}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Invoke an MCP action through the ArmorIQ proxy with token verification.
|
|
314
|
+
*/
|
|
315
|
+
async invoke(mcp, action, intentToken, params, merkleProof, userEmail) {
|
|
316
|
+
console.log(`Invoking MCP action: mcp=${mcp}, action=${action}`);
|
|
317
|
+
// Check token expiry
|
|
318
|
+
if (models_1.IntentToken.isExpired(intentToken)) {
|
|
319
|
+
throw new exceptions_1.TokenExpiredException(`Intent token expired ${Math.abs(models_1.IntentToken.timeUntilExpiry(intentToken)).toFixed(1)}s ago`, intentToken.tokenId, intentToken.expiresAt);
|
|
320
|
+
}
|
|
321
|
+
// Get proxy endpoint for this MCP
|
|
322
|
+
const proxyUrl = this.proxyEndpoints[mcp] ||
|
|
323
|
+
process.env[`${mcp.toUpperCase()}_PROXY_URL`] ||
|
|
324
|
+
this.defaultProxyEndpoint;
|
|
325
|
+
// Build IAM context
|
|
326
|
+
const iamContext = {};
|
|
327
|
+
if (intentToken.policyValidation) {
|
|
328
|
+
iamContext.allowed_tools = intentToken.policyValidation.allowed_tools || [];
|
|
329
|
+
}
|
|
330
|
+
if (userEmail) {
|
|
331
|
+
iamContext.email = userEmail;
|
|
332
|
+
iamContext.user_email = userEmail;
|
|
333
|
+
}
|
|
334
|
+
if (intentToken.rawToken) {
|
|
335
|
+
iamContext.user_id = intentToken.rawToken.user_id || this.userId;
|
|
336
|
+
iamContext.agent_id = intentToken.rawToken.agent_id || this.agentId;
|
|
337
|
+
}
|
|
338
|
+
// Prepare invocation payload (clean tool params only — proxy handles IAM injection)
|
|
339
|
+
const invokeParams = { ...(params || {}) };
|
|
340
|
+
const payload = {
|
|
341
|
+
mcp,
|
|
342
|
+
action,
|
|
343
|
+
tool: action,
|
|
344
|
+
params: invokeParams,
|
|
345
|
+
arguments: invokeParams,
|
|
346
|
+
intent_token: intentToken.rawToken,
|
|
347
|
+
merkle_proof: merkleProof,
|
|
348
|
+
plan: intentToken.rawToken?.plan,
|
|
349
|
+
_iam_context: iamContext,
|
|
350
|
+
...(userEmail ? { user_email: userEmail } : {}),
|
|
351
|
+
...(intentToken.policySnapshot ? { policy_snapshot: intentToken.policySnapshot } : {}),
|
|
352
|
+
};
|
|
353
|
+
// Prepare headers
|
|
354
|
+
const headers = {
|
|
355
|
+
Accept: 'application/json, text/event-stream',
|
|
356
|
+
'Content-Type': 'application/json',
|
|
357
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
358
|
+
'X-Request-ID': `sdk-${Date.now()}`,
|
|
359
|
+
};
|
|
360
|
+
if (this.apiKey) {
|
|
361
|
+
headers['X-API-Key'] = this.apiKey;
|
|
362
|
+
}
|
|
363
|
+
// Send CSRG token structure
|
|
364
|
+
if (intentToken.rawToken && typeof intentToken.rawToken === 'object') {
|
|
365
|
+
payload.token = intentToken.rawToken.token || {};
|
|
366
|
+
payload.csrg_token = intentToken.rawToken.token || {};
|
|
367
|
+
}
|
|
368
|
+
// Find step index for this action
|
|
369
|
+
const plan = intentToken.rawToken?.plan || {};
|
|
370
|
+
const steps = plan.steps || [];
|
|
371
|
+
let stepIndex = null;
|
|
372
|
+
for (let idx = 0; idx < steps.length; idx++) {
|
|
373
|
+
if (typeof steps[idx] === 'object' && steps[idx].action === action) {
|
|
374
|
+
stepIndex = idx;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (stepIndex === null) {
|
|
379
|
+
const actions = steps.map((s) => (typeof s === 'object' ? s.action : 'unknown'));
|
|
380
|
+
throw new exceptions_1.IntentMismatchException(`Action '${action}' not found in the original plan. ` +
|
|
381
|
+
`Plan contains actions: ${JSON.stringify(actions)}. ` +
|
|
382
|
+
'You can only invoke actions that were included in the plan when you called capturePlan().');
|
|
383
|
+
}
|
|
384
|
+
// Use Merkle proof from CSRG-IAP
|
|
385
|
+
if (!merkleProof) {
|
|
386
|
+
if (intentToken.stepProofs && intentToken.stepProofs.length > stepIndex) {
|
|
387
|
+
merkleProof = intentToken.stepProofs[stepIndex];
|
|
388
|
+
console.log(`Using Merkle proof from CSRG-IAP for step ${stepIndex}`);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
console.warn(`No Merkle proof available for step ${stepIndex}. ` +
|
|
392
|
+
`step_proofs length: ${intentToken.stepProofs?.length || 0}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Add CSRG proof to headers
|
|
396
|
+
if (merkleProof) {
|
|
397
|
+
headers['X-CSRG-Proof'] = JSON.stringify(merkleProof);
|
|
398
|
+
}
|
|
399
|
+
const csrgPath = `/steps/[${stepIndex}]/action`;
|
|
400
|
+
headers['X-CSRG-Path'] = csrgPath;
|
|
401
|
+
// Calculate value digest
|
|
402
|
+
const stepObj = steps[stepIndex] || {};
|
|
403
|
+
const leafValue = typeof stepObj === 'object' ? stepObj.action || action : action;
|
|
404
|
+
const valueStr = JSON.stringify(leafValue);
|
|
405
|
+
const valueDigest = crypto.createHash('sha256').update(valueStr, 'utf8').digest('hex');
|
|
406
|
+
headers['X-CSRG-Value-Digest'] = valueDigest;
|
|
407
|
+
// Call proxy
|
|
408
|
+
try {
|
|
409
|
+
const startTime = Date.now();
|
|
410
|
+
const response = await this.httpClient.post(`${proxyUrl}/invoke`, payload, { headers });
|
|
411
|
+
const executionTime = (Date.now() - startTime) / 1000;
|
|
412
|
+
const responseData = response.data;
|
|
413
|
+
// Check for enforcement responses (hold/block) BEFORE generic error handling
|
|
414
|
+
if (responseData?.enforcement) {
|
|
415
|
+
const enforcement = responseData.enforcement;
|
|
416
|
+
const enrichedError = new Error(responseData.message || `Enforcement: ${enforcement.action}`);
|
|
417
|
+
enrichedError.response = { status: response.status, data: responseData };
|
|
418
|
+
enrichedError.name = enforcement.action === 'block' ? 'PolicyBlockedException' : 'PolicyHoldException';
|
|
419
|
+
throw enrichedError;
|
|
420
|
+
}
|
|
421
|
+
if (response.status >= 400) {
|
|
422
|
+
const enrichedError = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
423
|
+
enrichedError.response = { status: response.status, data: responseData };
|
|
424
|
+
throw enrichedError;
|
|
425
|
+
}
|
|
426
|
+
const contentType = response.headers['content-type'] || '';
|
|
427
|
+
let data;
|
|
428
|
+
// Handle SSE format
|
|
429
|
+
if (contentType.includes('text/event-stream') && typeof responseData === 'string') {
|
|
430
|
+
const lines = responseData.split('\n');
|
|
431
|
+
for (const line of lines) {
|
|
432
|
+
if (line.startsWith('data: ')) {
|
|
433
|
+
try {
|
|
434
|
+
data = JSON.parse(line.slice(6));
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
catch (e) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (!data) {
|
|
443
|
+
throw new exceptions_1.MCPInvocationException('No data in SSE response', mcp, action);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
data = responseData;
|
|
448
|
+
}
|
|
449
|
+
// Check for JSON-RPC error
|
|
450
|
+
if (data.error && !data.enforcement) {
|
|
451
|
+
const errorMsg = data.error.message || 'Unknown error';
|
|
452
|
+
const errorCode = data.error.code || -1;
|
|
453
|
+
const errorData = data.error.data || '';
|
|
454
|
+
throw new exceptions_1.MCPInvocationException(`MCP tool error (${errorCode}): ${errorMsg} - ${errorData}`, mcp, action);
|
|
455
|
+
}
|
|
456
|
+
const resultData = data.result || data;
|
|
457
|
+
const hasToolError = Boolean(resultData?.isError) ||
|
|
458
|
+
Boolean(resultData?.error) ||
|
|
459
|
+
Boolean(resultData?.is_error);
|
|
460
|
+
const result = {
|
|
461
|
+
mcp,
|
|
462
|
+
action,
|
|
463
|
+
result: resultData,
|
|
464
|
+
status: hasToolError ? 'error' : 'success',
|
|
465
|
+
executionTime,
|
|
466
|
+
verified: true,
|
|
467
|
+
metadata: {
|
|
468
|
+
hasToolError,
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
console.log(`MCP invocation ${hasToolError ? 'returned error payload' : 'succeeded'}: ${action} in ${executionTime.toFixed(2)}s`);
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
if (error instanceof exceptions_1.MCPInvocationException) {
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
// Re-throw enforcement errors so invokeWithPolicy() can catch them
|
|
479
|
+
if (error.name === 'PolicyBlockedException' || error.name === 'PolicyHoldException') {
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
const statusCode = error.response?.status;
|
|
483
|
+
const errorDetail = error.response?.data || error.message;
|
|
484
|
+
// Check if the 403 response is actually an enforcement response
|
|
485
|
+
if (statusCode === 403 && errorDetail?.enforcement) {
|
|
486
|
+
const enrichedError = new Error(errorDetail.message || 'Enforcement action');
|
|
487
|
+
enrichedError.response = error.response;
|
|
488
|
+
enrichedError.name = errorDetail.enforcement.action === 'block' ? 'PolicyBlockedException' : 'PolicyHoldException';
|
|
489
|
+
throw enrichedError;
|
|
490
|
+
}
|
|
491
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
492
|
+
throw new exceptions_1.InvalidTokenException(`Token verification failed: ${errorDetail}`);
|
|
493
|
+
}
|
|
494
|
+
else if (statusCode === 409) {
|
|
495
|
+
throw new exceptions_1.IntentMismatchException(`Action not in plan: ${errorDetail}`, action, intentToken.planHash);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
throw new exceptions_1.MCPInvocationException(`MCP invocation failed: ${errorDetail}`, mcp, action, statusCode);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Delegate authority to another agent using CSRG token delegation.
|
|
504
|
+
*/
|
|
505
|
+
async delegate(intentToken, delegatePublicKey, validitySeconds = 3600, allowedActions, targetAgent, subtask) {
|
|
506
|
+
console.log(`Creating delegation for token_id=${intentToken.tokenId}, ` +
|
|
507
|
+
`delegate_key=${delegatePublicKey.slice(0, 16)}..., validity=${validitySeconds}s`);
|
|
508
|
+
let tokenToDelegate = intentToken.rawToken;
|
|
509
|
+
if (typeof tokenToDelegate === 'object' && 'token' in tokenToDelegate) {
|
|
510
|
+
tokenToDelegate = tokenToDelegate.token;
|
|
511
|
+
}
|
|
512
|
+
const payload = {
|
|
513
|
+
token: tokenToDelegate,
|
|
514
|
+
delegate_public_key: delegatePublicKey,
|
|
515
|
+
validity_seconds: validitySeconds,
|
|
516
|
+
};
|
|
517
|
+
if (allowedActions) {
|
|
518
|
+
payload.allowed_actions = allowedActions;
|
|
519
|
+
}
|
|
520
|
+
if (targetAgent) {
|
|
521
|
+
payload.target_agent = targetAgent;
|
|
522
|
+
}
|
|
523
|
+
if (subtask) {
|
|
524
|
+
payload.subtask = subtask;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const response = await this.httpClient.post(`${this.iapEndpoint}/delegation/create`, payload, {
|
|
528
|
+
timeout: 10000,
|
|
529
|
+
});
|
|
530
|
+
if (response.status >= 400) {
|
|
531
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
532
|
+
}
|
|
533
|
+
const data = response.data;
|
|
534
|
+
const delegatedTokenData = data.delegation || data.delegated_token || data.new_token;
|
|
535
|
+
if (!delegatedTokenData) {
|
|
536
|
+
throw new exceptions_1.DelegationException(`Delegation response missing 'delegation' key. Got keys: ${Object.keys(data).join(', ')}`, undefined, data.delegation_id);
|
|
537
|
+
}
|
|
538
|
+
const delegatedToken = {
|
|
539
|
+
tokenId: delegatedTokenData.token_id || '',
|
|
540
|
+
planHash: delegatedTokenData.plan_hash || intentToken.planHash,
|
|
541
|
+
planId: delegatedTokenData.plan_id,
|
|
542
|
+
signature: delegatedTokenData.signature || '',
|
|
543
|
+
issuedAt: delegatedTokenData.issued_at || Date.now() / 1000,
|
|
544
|
+
expiresAt: delegatedTokenData.expires_at || 0,
|
|
545
|
+
policy: delegatedTokenData.policy || {},
|
|
546
|
+
compositeIdentity: delegatedTokenData.composite_identity || '',
|
|
547
|
+
clientInfo: delegatedTokenData.client_info,
|
|
548
|
+
policyValidation: delegatedTokenData.policy_validation,
|
|
549
|
+
stepProofs: delegatedTokenData.step_proofs || [],
|
|
550
|
+
totalSteps: delegatedTokenData.total_steps || 0,
|
|
551
|
+
rawToken: { token: delegatedTokenData },
|
|
552
|
+
};
|
|
553
|
+
const result = {
|
|
554
|
+
delegationId: data.delegation_id || delegatedToken.tokenId,
|
|
555
|
+
delegatedToken,
|
|
556
|
+
delegatePublicKey,
|
|
557
|
+
targetAgent,
|
|
558
|
+
expiresAt: delegatedToken.expiresAt,
|
|
559
|
+
trustDelta: data.trust_delta || {},
|
|
560
|
+
status: 'delegated',
|
|
561
|
+
metadata: data.metadata || {},
|
|
562
|
+
};
|
|
563
|
+
console.log(`Delegation successful: delegation_id=${result.delegationId}`);
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
const statusCode = error.response?.status;
|
|
568
|
+
const errorDetail = error.response?.data || error.message;
|
|
569
|
+
throw new exceptions_1.DelegationException(`Delegation failed: ${errorDetail}`, targetAgent, undefined, statusCode);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Verify an intent token with IAP.
|
|
574
|
+
*/
|
|
575
|
+
async verifyToken(intentToken) {
|
|
576
|
+
try {
|
|
577
|
+
if (models_1.IntentToken.isExpired(intentToken)) {
|
|
578
|
+
console.warn(`Token ${intentToken.tokenId} has expired`);
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
if (!intentToken.signature || !intentToken.planHash) {
|
|
582
|
+
console.warn(`Token ${intentToken.tokenId} missing required fields`);
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
console.log(`Token ${intentToken.tokenId} is valid (expires in ${models_1.IntentToken.timeUntilExpiry(intentToken).toFixed(1)}s)`);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
console.error(`Token verification failed: ${error.message}`);
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// ─── Semantic Metadata & Policy Context ──────────────────────────
|
|
594
|
+
/**
|
|
595
|
+
* Fetch and cache semantic tool metadata for an MCP server.
|
|
596
|
+
*/
|
|
597
|
+
async fetchToolMetadata(mcpName) {
|
|
598
|
+
const cached = this.metadataCache.get(mcpName);
|
|
599
|
+
if (cached)
|
|
600
|
+
return cached;
|
|
601
|
+
try {
|
|
602
|
+
const response = await this.httpClient.get(`${this.backendEndpoint}/mcp/tool-metadata/${encodeURIComponent(mcpName)}`, { headers: { 'X-API-Key': this.apiKey } });
|
|
603
|
+
if (response.status >= 400) {
|
|
604
|
+
console.warn(`Failed to fetch tool metadata for ${mcpName}: ${response.status}`);
|
|
605
|
+
return { mcpId: '', name: mcpName, toolMetadata: {}, roleMapping: {} };
|
|
606
|
+
}
|
|
607
|
+
const metadata = response.data?.data || {
|
|
608
|
+
mcpId: '',
|
|
609
|
+
name: mcpName,
|
|
610
|
+
toolMetadata: {},
|
|
611
|
+
roleMapping: {},
|
|
612
|
+
};
|
|
613
|
+
this.metadataCache.set(mcpName, metadata);
|
|
614
|
+
return metadata;
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
console.warn(`Could not fetch tool metadata for ${mcpName}: ${error.message}`);
|
|
618
|
+
return { mcpId: '', name: mcpName, toolMetadata: {}, roleMapping: {} };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Eagerly load metadata for an MCP so subsequent invoke() calls are fast.
|
|
623
|
+
*/
|
|
624
|
+
async loadMcp(mcpName) {
|
|
625
|
+
await this.fetchToolMetadata(mcpName);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* List all MCPs registered for this org (resolved via API key).
|
|
629
|
+
*/
|
|
630
|
+
async listMcps() {
|
|
631
|
+
const response = await this.httpClient.get(`${this.backendEndpoint}/mcp/my-servers`, { headers: { 'X-API-Key': this.apiKey } });
|
|
632
|
+
if (response.status >= 400) {
|
|
633
|
+
throw new exceptions_1.MCPInvocationException(`listMcps failed: ${response.status} ${JSON.stringify(response.data)}`);
|
|
634
|
+
}
|
|
635
|
+
return response.data?.data || [];
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Get full OpenAI-compatible tool schemas for a named MCP.
|
|
639
|
+
*/
|
|
640
|
+
async getMcpToolSchemas(mcpName) {
|
|
641
|
+
const response = await this.httpClient.get(`${this.backendEndpoint}/mcp/tools/${encodeURIComponent(mcpName)}`, { headers: { 'X-API-Key': this.apiKey } });
|
|
642
|
+
if (response.status >= 400) {
|
|
643
|
+
throw new exceptions_1.MCPInvocationException(`getMcpToolSchemas(${mcpName}) failed: ${response.status} ${JSON.stringify(response.data)}`);
|
|
644
|
+
}
|
|
645
|
+
return response.data?.data?.tools || [];
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Enrich policy context from semantic metadata for a tool invocation.
|
|
649
|
+
*/
|
|
650
|
+
async enrichPolicyContext(mcpName, toolName, params) {
|
|
651
|
+
const metadata = await this.fetchToolMetadata(mcpName);
|
|
652
|
+
const toolMeta = metadata.toolMetadata?.[toolName];
|
|
653
|
+
if (!toolMeta || !toolMeta.isFinancial) {
|
|
654
|
+
return { is_financial: false };
|
|
655
|
+
}
|
|
656
|
+
let amount;
|
|
657
|
+
if (toolMeta.amountFields) {
|
|
658
|
+
for (const field of toolMeta.amountFields) {
|
|
659
|
+
if (params[field] != null) {
|
|
660
|
+
const raw = Number(params[field]);
|
|
661
|
+
amount = toolMeta.amountUnit === 'cents' ? raw / 100 : raw;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
is_financial: true,
|
|
668
|
+
transaction_type: toolMeta.transactionType || toolName,
|
|
669
|
+
amount,
|
|
670
|
+
recipient_id: toolMeta.recipientField ? params[toolMeta.recipientField] : undefined,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Resolve an org role to a domain-specific role for an MCP.
|
|
675
|
+
*/
|
|
676
|
+
async resolveRole(mcpName, orgRole) {
|
|
677
|
+
const metadata = await this.fetchToolMetadata(mcpName);
|
|
678
|
+
return metadata.roleMapping?.[orgRole] || orgRole;
|
|
679
|
+
}
|
|
680
|
+
// ─── Delegation Automation ──────────────────────────────────────
|
|
681
|
+
/**
|
|
682
|
+
* Resolve the user's role and limit from the backend for delegation.
|
|
683
|
+
*/
|
|
684
|
+
async resolveUserRole(userEmail) {
|
|
685
|
+
try {
|
|
686
|
+
const response = await this.httpClient.get(`${this.backendEndpoint}/delegation/my-role`, {
|
|
687
|
+
params: {},
|
|
688
|
+
headers: { 'X-API-Key': this.apiKey, 'X-User-Email': userEmail },
|
|
689
|
+
timeout: 5000,
|
|
690
|
+
});
|
|
691
|
+
if (response.status < 400 && response.data?.role) {
|
|
692
|
+
return {
|
|
693
|
+
role: response.data.role,
|
|
694
|
+
limit: response.data.limit ?? 0,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
catch (e) {
|
|
699
|
+
// Fallback below
|
|
700
|
+
}
|
|
701
|
+
return { role: 'agent_user', limit: 0 };
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Create a delegation request on the backend.
|
|
705
|
+
*/
|
|
706
|
+
async createDelegationRequest(params) {
|
|
707
|
+
const response = await this.httpClient.post(`${this.backendEndpoint}/delegation/request`, params, { headers: { 'X-API-Key': this.apiKey, 'X-User-Email': params.requesterEmail } });
|
|
708
|
+
if (response.status >= 400) {
|
|
709
|
+
throw new exceptions_1.DelegationException(`Failed to create delegation request: ${response.data?.message || response.statusText}`);
|
|
710
|
+
}
|
|
711
|
+
return response.data;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Check if a delegation request has been approved.
|
|
715
|
+
*/
|
|
716
|
+
async checkApprovedDelegation(userEmail, tool, amount) {
|
|
717
|
+
const response = await this.httpClient.get(`${this.backendEndpoint}/delegation/check-approved`, {
|
|
718
|
+
params: { tool, amount },
|
|
719
|
+
headers: { 'X-API-Key': this.apiKey, 'X-User-Email': userEmail },
|
|
720
|
+
});
|
|
721
|
+
if (response.status >= 400 || !response.data?.approved) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
return response.data;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Mark a delegation as executed.
|
|
728
|
+
*/
|
|
729
|
+
async markDelegationExecuted(userEmail, delegationId) {
|
|
730
|
+
await this.httpClient.post(`${this.backendEndpoint}/delegation/mark-executed`, { delegationId }, { headers: { 'X-API-Key': this.apiKey, 'X-User-Email': userEmail } });
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Mark an intent plan as completed after all tools have been executed.
|
|
734
|
+
*/
|
|
735
|
+
async completePlan(planId) {
|
|
736
|
+
await this.updatePlanStatus(planId, 'completed');
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Update an intent plan's status.
|
|
740
|
+
* Valid statuses: 'active', 'completed', 'failed', 'expired'
|
|
741
|
+
*/
|
|
742
|
+
async updatePlanStatus(planId, status) {
|
|
743
|
+
try {
|
|
744
|
+
await this.httpClient.post(`${this.backendEndpoint}/iap/plans/${planId}/status`, { status }, { headers: { 'X-API-Key': this.apiKey } });
|
|
745
|
+
console.log(`Plan ${planId} status updated to ${status}`);
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
console.warn(`Failed to update plan ${planId} status: ${error.message}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Enhanced invoke with automatic policy context enrichment and delegation handling.
|
|
753
|
+
*/
|
|
754
|
+
async invokeWithPolicy(mcp, action, intentToken, params, options) {
|
|
755
|
+
// Enrich with policy context from semantic metadata
|
|
756
|
+
const policyContext = await this.enrichPolicyContext(mcp, action, params || {});
|
|
757
|
+
const enrichedParams = {
|
|
758
|
+
...(params || {}),
|
|
759
|
+
_policy_context: policyContext,
|
|
760
|
+
};
|
|
761
|
+
try {
|
|
762
|
+
return await this.invoke(mcp, action, intentToken, enrichedParams, undefined, options?.userEmail);
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
// Detect structured enforcement responses
|
|
766
|
+
const responseData = error.response?.data;
|
|
767
|
+
if (!responseData?.enforcement)
|
|
768
|
+
throw error;
|
|
769
|
+
const enforcement = responseData.enforcement;
|
|
770
|
+
if (enforcement.action === 'block') {
|
|
771
|
+
throw new exceptions_1.PolicyBlockedException(responseData.message || 'Action blocked by policy', enforcement.action, enforcement.reason, enforcement.metadata);
|
|
772
|
+
}
|
|
773
|
+
if (enforcement.action === 'hold' && enforcement.requiresApproval) {
|
|
774
|
+
const holdInfo = {
|
|
775
|
+
reason: enforcement.reason,
|
|
776
|
+
amount: enforcement.metadata?.amount,
|
|
777
|
+
approvalThreshold: enforcement.metadata?.approvalThreshold,
|
|
778
|
+
tool: action,
|
|
779
|
+
mcp,
|
|
780
|
+
};
|
|
781
|
+
options?.onHold?.(holdInfo);
|
|
782
|
+
if (!options?.waitForApproval || !options?.userEmail) {
|
|
783
|
+
throw new exceptions_1.PolicyHoldException(responseData.message || 'Action held for approval', responseData.delegation_context, enforcement.metadata);
|
|
784
|
+
}
|
|
785
|
+
// Auto-create delegation request
|
|
786
|
+
const delegationCtx = responseData.delegation_context || {};
|
|
787
|
+
// Resolve requester role/limit: use options, or auto-resolve from backend
|
|
788
|
+
let requesterRole = options.requesterRole || 'agent_user';
|
|
789
|
+
let requesterLimit = options.requesterLimit ?? 0;
|
|
790
|
+
if (!options.requesterRole) {
|
|
791
|
+
try {
|
|
792
|
+
const resolved = await this.resolveUserRole(options.userEmail);
|
|
793
|
+
requesterRole = resolved.role;
|
|
794
|
+
requesterLimit = resolved.limit;
|
|
795
|
+
}
|
|
796
|
+
catch (_e) {
|
|
797
|
+
// Use defaults
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
let delegationResult;
|
|
801
|
+
try {
|
|
802
|
+
delegationResult = await this.createDelegationRequest({
|
|
803
|
+
tool: action,
|
|
804
|
+
action: 'execute',
|
|
805
|
+
arguments: params || {},
|
|
806
|
+
amount: policyContext.amount ?? 0,
|
|
807
|
+
requesterEmail: options.userEmail,
|
|
808
|
+
requesterRole,
|
|
809
|
+
requesterLimit,
|
|
810
|
+
domain: delegationCtx.domain || mcp,
|
|
811
|
+
targetUrl: delegationCtx.targetUrl,
|
|
812
|
+
planId: delegationCtx.planId || intentToken.planId,
|
|
813
|
+
intentReference: delegationCtx.intentReference || intentToken.tokenId,
|
|
814
|
+
merkleRoot: delegationCtx.merkleRoot || intentToken.planHash,
|
|
815
|
+
reason: enforcement.reason,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
catch (delegationError) {
|
|
819
|
+
console.error(`Failed to create delegation request: ${delegationError.message}`);
|
|
820
|
+
throw new exceptions_1.PolicyHoldException(responseData.message || 'Action held for approval (delegation request failed)', responseData.delegation_context, { ...enforcement.metadata, delegationError: delegationError.message });
|
|
821
|
+
}
|
|
822
|
+
holdInfo.delegationId = delegationResult.delegationId;
|
|
823
|
+
options?.onHold?.(holdInfo);
|
|
824
|
+
// Poll for approval
|
|
825
|
+
const timeout = options.delegationTimeoutMs || 30 * 60 * 1000;
|
|
826
|
+
const startTime = Date.now();
|
|
827
|
+
let pollInterval = 3000;
|
|
828
|
+
while (Date.now() - startTime < timeout) {
|
|
829
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
830
|
+
pollInterval = Math.min(pollInterval * 1.5, 15000);
|
|
831
|
+
const approved = await this.checkApprovedDelegation(options.userEmail, action, policyContext.amount || 0);
|
|
832
|
+
if (approved) {
|
|
833
|
+
// Retry with delegation context
|
|
834
|
+
const retryParams = {
|
|
835
|
+
...enrichedParams,
|
|
836
|
+
_delegation_id: approved.delegationId,
|
|
837
|
+
_approvals: 999,
|
|
838
|
+
};
|
|
839
|
+
const result = await this.invoke(mcp, action, intentToken, retryParams, undefined, options.userEmail);
|
|
840
|
+
await this.markDelegationExecuted(options.userEmail, approved.delegationId);
|
|
841
|
+
return result;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
throw new exceptions_1.DelegationException(`Delegation approval timed out after ${timeout / 1000}s`, undefined, delegationResult.delegationId);
|
|
845
|
+
}
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Close HTTP client and cleanup resources.
|
|
851
|
+
*/
|
|
852
|
+
close() {
|
|
853
|
+
// Axios doesn't require explicit cleanup
|
|
854
|
+
console.log('ArmorIQ SDK client closed');
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
exports.ArmorIQClient = ArmorIQClient;
|
|
858
|
+
//# sourceMappingURL=client.js.map
|