@axonflow/sdk 2.2.0 → 3.3.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 +185 -8
- package/dist/cjs/client.d.ts +530 -8
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +1760 -45
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/errors.d.ts +22 -0
- package/dist/cjs/errors.d.ts.map +1 -1
- package/dist/cjs/errors.js +32 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.d.ts +10 -14
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +11 -20
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/connector.d.ts +82 -0
- package/dist/cjs/types/connector.d.ts.map +1 -1
- package/dist/cjs/types/connector.js +7 -0
- package/dist/cjs/types/connector.js.map +1 -1
- package/dist/cjs/types/execution.d.ts +227 -0
- package/dist/cjs/types/execution.d.ts.map +1 -0
- package/dist/cjs/types/execution.js +73 -0
- package/dist/cjs/types/execution.js.map +1 -0
- package/dist/cjs/types/index.d.ts +3 -0
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/index.js +3 -0
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/types/masfeat.d.ts +238 -0
- package/dist/cjs/types/masfeat.d.ts.map +1 -0
- package/dist/cjs/types/masfeat.js +11 -0
- package/dist/cjs/types/masfeat.js.map +1 -0
- package/dist/cjs/types/planning.d.ts +126 -1
- package/dist/cjs/types/planning.d.ts.map +1 -1
- package/dist/cjs/types/policies.d.ts +19 -1
- package/dist/cjs/types/policies.d.ts.map +1 -1
- package/dist/cjs/types/proxy.d.ts +29 -3
- package/dist/cjs/types/proxy.d.ts.map +1 -1
- package/dist/cjs/types/workflows.d.ts +318 -0
- package/dist/cjs/types/workflows.d.ts.map +1 -0
- package/dist/cjs/types/workflows.js +61 -0
- package/dist/cjs/types/workflows.js.map +1 -0
- package/dist/esm/client.d.ts +530 -8
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +1761 -46
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/errors.d.ts +22 -0
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +30 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +10 -14
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +7 -15
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/connector.d.ts +82 -0
- package/dist/esm/types/connector.d.ts.map +1 -1
- package/dist/esm/types/connector.js +6 -1
- package/dist/esm/types/connector.js.map +1 -1
- package/dist/esm/types/execution.d.ts +227 -0
- package/dist/esm/types/execution.d.ts.map +1 -0
- package/dist/esm/types/execution.js +70 -0
- package/dist/esm/types/execution.js.map +1 -0
- package/dist/esm/types/index.d.ts +3 -0
- package/dist/esm/types/index.d.ts.map +1 -1
- package/dist/esm/types/index.js +3 -0
- package/dist/esm/types/index.js.map +1 -1
- package/dist/esm/types/masfeat.d.ts +238 -0
- package/dist/esm/types/masfeat.d.ts.map +1 -0
- package/dist/esm/types/masfeat.js +10 -0
- package/dist/esm/types/masfeat.js.map +1 -0
- package/dist/esm/types/planning.d.ts +126 -1
- package/dist/esm/types/planning.d.ts.map +1 -1
- package/dist/esm/types/policies.d.ts +19 -1
- package/dist/esm/types/policies.d.ts.map +1 -1
- package/dist/esm/types/proxy.d.ts +29 -3
- package/dist/esm/types/proxy.d.ts.map +1 -1
- package/dist/esm/types/workflows.d.ts +318 -0
- package/dist/esm/types/workflows.d.ts.map +1 -0
- package/dist/esm/types/workflows.js +58 -0
- package/dist/esm/types/workflows.js.map +1 -0
- package/package.json +7 -2
- package/dist/cjs/interceptors/anthropic.d.ts +0 -40
- package/dist/cjs/interceptors/anthropic.d.ts.map +0 -1
- package/dist/cjs/interceptors/anthropic.js +0 -101
- package/dist/cjs/interceptors/anthropic.js.map +0 -1
- package/dist/cjs/interceptors/base.d.ts +0 -23
- package/dist/cjs/interceptors/base.d.ts.map +0 -1
- package/dist/cjs/interceptors/base.js +0 -10
- package/dist/cjs/interceptors/base.js.map +0 -1
- package/dist/cjs/interceptors/bedrock.d.ts +0 -142
- package/dist/cjs/interceptors/bedrock.d.ts.map +0 -1
- package/dist/cjs/interceptors/bedrock.js +0 -263
- package/dist/cjs/interceptors/bedrock.js.map +0 -1
- package/dist/cjs/interceptors/gemini.d.ts +0 -89
- package/dist/cjs/interceptors/gemini.d.ts.map +0 -1
- package/dist/cjs/interceptors/gemini.js +0 -121
- package/dist/cjs/interceptors/gemini.js.map +0 -1
- package/dist/cjs/interceptors/ollama.d.ts +0 -143
- package/dist/cjs/interceptors/ollama.d.ts.map +0 -1
- package/dist/cjs/interceptors/ollama.js +0 -153
- package/dist/cjs/interceptors/ollama.js.map +0 -1
- package/dist/cjs/interceptors/openai.d.ts +0 -40
- package/dist/cjs/interceptors/openai.d.ts.map +0 -1
- package/dist/cjs/interceptors/openai.js +0 -100
- package/dist/cjs/interceptors/openai.js.map +0 -1
- package/dist/esm/interceptors/anthropic.d.ts +0 -40
- package/dist/esm/interceptors/anthropic.d.ts.map +0 -1
- package/dist/esm/interceptors/anthropic.js +0 -96
- package/dist/esm/interceptors/anthropic.js.map +0 -1
- package/dist/esm/interceptors/base.d.ts +0 -23
- package/dist/esm/interceptors/base.d.ts.map +0 -1
- package/dist/esm/interceptors/base.js +0 -6
- package/dist/esm/interceptors/base.js.map +0 -1
- package/dist/esm/interceptors/bedrock.d.ts +0 -142
- package/dist/esm/interceptors/bedrock.d.ts.map +0 -1
- package/dist/esm/interceptors/bedrock.js +0 -224
- package/dist/esm/interceptors/bedrock.js.map +0 -1
- package/dist/esm/interceptors/gemini.d.ts +0 -89
- package/dist/esm/interceptors/gemini.d.ts.map +0 -1
- package/dist/esm/interceptors/gemini.js +0 -116
- package/dist/esm/interceptors/gemini.js.map +0 -1
- package/dist/esm/interceptors/ollama.d.ts +0 -143
- package/dist/esm/interceptors/ollama.d.ts.map +0 -1
- package/dist/esm/interceptors/ollama.js +0 -147
- package/dist/esm/interceptors/ollama.js.map +0 -1
- package/dist/esm/interceptors/openai.d.ts +0 -40
- package/dist/esm/interceptors/openai.d.ts.map +0 -1
- package/dist/esm/interceptors/openai.js +0 -95
- package/dist/esm/interceptors/openai.js.map +0 -1
package/dist/esm/client.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { AuthenticationError, APIError, PolicyViolationError, ConfigurationError, ConnectorError, PlanExecutionError, } from './errors.js';
|
|
2
|
-
import { OpenAIInterceptor } from './interceptors/openai.js';
|
|
3
|
-
import { AnthropicInterceptor } from './interceptors/anthropic.js';
|
|
1
|
+
import { AuthenticationError, APIError, PolicyViolationError, ConfigurationError, ConnectorError, PlanExecutionError, VersionConflictError, } from './errors.js';
|
|
4
2
|
import { generateRequestId, debugLog } from './utils/helpers.js';
|
|
5
3
|
/**
|
|
6
4
|
* Main AxonFlow client for invisible AI governance
|
|
@@ -24,7 +22,7 @@ export class AxonFlow {
|
|
|
24
22
|
clientSecret: config.clientSecret,
|
|
25
23
|
endpoint,
|
|
26
24
|
mode: config.mode || (hasCredentials ? 'production' : 'sandbox'),
|
|
27
|
-
tenant: config.tenant || '
|
|
25
|
+
tenant: config.tenant || '',
|
|
28
26
|
debug: config.debug || false,
|
|
29
27
|
timeout: config.timeout || 30000,
|
|
30
28
|
mapTimeout: config.mapTimeout || 120000, // 2 minutes for MAP operations
|
|
@@ -38,8 +36,8 @@ export class AxonFlow {
|
|
|
38
36
|
ttl: config.cache?.ttl || 60000,
|
|
39
37
|
},
|
|
40
38
|
};
|
|
41
|
-
//
|
|
42
|
-
this.interceptors = [
|
|
39
|
+
// Interceptors removed in v3.0.0 (deprecated wrapOpenAIClient/wrapAnthropicClient)
|
|
40
|
+
this.interceptors = [];
|
|
43
41
|
if (this.config.debug) {
|
|
44
42
|
// Determine auth method for logging
|
|
45
43
|
const authMethod = hasCredentials ? 'client-credentials' : 'community (no auth)';
|
|
@@ -64,10 +62,25 @@ export class AxonFlow {
|
|
|
64
62
|
if (this.config.clientId && this.config.clientSecret) {
|
|
65
63
|
const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
|
|
66
64
|
headers['Authorization'] = `Basic ${credentials}`;
|
|
65
|
+
}
|
|
66
|
+
// Always add X-Tenant-ID when clientId is set (required for multi-tenant APIs)
|
|
67
|
+
if (this.config.clientId) {
|
|
67
68
|
headers['X-Tenant-ID'] = this.config.clientId;
|
|
68
69
|
}
|
|
69
70
|
return headers;
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Get the effective clientId, using smart default for community mode.
|
|
74
|
+
*
|
|
75
|
+
* Returns the configured clientId if set, otherwise returns "community"
|
|
76
|
+
* as a smart default. This enables zero-config usage for community/self-hosted
|
|
77
|
+
* deployments while still supporting enterprise deployments with explicit credentials.
|
|
78
|
+
*
|
|
79
|
+
* @returns The clientId to use in requests
|
|
80
|
+
*/
|
|
81
|
+
getEffectiveClientId() {
|
|
82
|
+
return this.config.clientId || this.config.tenant || 'community';
|
|
83
|
+
}
|
|
71
84
|
/**
|
|
72
85
|
* Main method to protect AI calls with governance
|
|
73
86
|
* @param aiCall The AI call to protect
|
|
@@ -105,7 +118,7 @@ export class AxonFlow {
|
|
|
105
118
|
*
|
|
106
119
|
* **Proxy Mode:**
|
|
107
120
|
* ```typescript
|
|
108
|
-
* const response = await axonflow.
|
|
121
|
+
* const response = await axonflow.proxyLLMCall({
|
|
109
122
|
* userToken: 'user-123',
|
|
110
123
|
* query: 'Your prompt here',
|
|
111
124
|
* requestType: 'chat'
|
|
@@ -115,8 +128,8 @@ export class AxonFlow {
|
|
|
115
128
|
* See: https://docs.getaxonflow.com/sdk/gateway-mode
|
|
116
129
|
*/
|
|
117
130
|
async protect(aiCall) {
|
|
118
|
-
console.warn('[AxonFlow] protect() is deprecated and will be removed in
|
|
119
|
-
'Use Gateway Mode (getPolicyApprovedContext + auditLLMCall) or Proxy Mode (
|
|
131
|
+
console.warn('[AxonFlow] protect() is deprecated and will be removed in a future version. ' +
|
|
132
|
+
'Use Gateway Mode (getPolicyApprovedContext + auditLLMCall) or Proxy Mode (proxyLLMCall) instead. ' +
|
|
120
133
|
'See: https://docs.getaxonflow.com/sdk/gateway-mode');
|
|
121
134
|
try {
|
|
122
135
|
// Extract request details from the AI call
|
|
@@ -377,10 +390,19 @@ export class AxonFlow {
|
|
|
377
390
|
}
|
|
378
391
|
}
|
|
379
392
|
/**
|
|
380
|
-
*
|
|
393
|
+
* Send a query through AxonFlow with full policy enforcement (Proxy Mode).
|
|
394
|
+
*
|
|
395
|
+
* This is Proxy Mode - AxonFlow acts as an intermediary, making the LLM call on your behalf.
|
|
381
396
|
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
397
|
+
* Use this when you want AxonFlow to:
|
|
398
|
+
* - Evaluate policies before the LLM call
|
|
399
|
+
* - Make the LLM call to the configured provider
|
|
400
|
+
* - Filter/redact sensitive data from responses
|
|
401
|
+
* - Automatically track costs and audit the interaction
|
|
402
|
+
*
|
|
403
|
+
* For Gateway Mode (lower latency, you make the LLM call), use:
|
|
404
|
+
* - getPolicyApprovedContext() before your LLM call
|
|
405
|
+
* - auditLLMCall() after your LLM call
|
|
384
406
|
*
|
|
385
407
|
* @param options - Query execution options
|
|
386
408
|
* @returns ExecuteQueryResponse with results or error information
|
|
@@ -390,7 +412,7 @@ export class AxonFlow {
|
|
|
390
412
|
*
|
|
391
413
|
* @example
|
|
392
414
|
* ```typescript
|
|
393
|
-
* const response = await axonflow.
|
|
415
|
+
* const response = await axonflow.proxyLLMCall({
|
|
394
416
|
* userToken: 'user-123',
|
|
395
417
|
* query: 'Explain quantum computing',
|
|
396
418
|
* requestType: 'chat',
|
|
@@ -402,13 +424,13 @@ export class AxonFlow {
|
|
|
402
424
|
* }
|
|
403
425
|
* ```
|
|
404
426
|
*/
|
|
405
|
-
async
|
|
427
|
+
async proxyLLMCall(options) {
|
|
406
428
|
// Default to "anonymous" if userToken is empty/undefined (community mode)
|
|
407
429
|
const effectiveUserToken = options.userToken || 'anonymous';
|
|
408
430
|
const agentRequest = {
|
|
409
431
|
query: options.query,
|
|
410
432
|
user_token: effectiveUserToken,
|
|
411
|
-
client_id: this.config.tenant,
|
|
433
|
+
client_id: this.config.clientId || this.config.tenant,
|
|
412
434
|
request_type: options.requestType,
|
|
413
435
|
context: options.context || {},
|
|
414
436
|
};
|
|
@@ -418,7 +440,7 @@ export class AxonFlow {
|
|
|
418
440
|
...this.getAuthHeaders(),
|
|
419
441
|
};
|
|
420
442
|
if (this.config.debug) {
|
|
421
|
-
debugLog('Proxy Mode:
|
|
443
|
+
debugLog('Proxy Mode: proxyLLMCall', {
|
|
422
444
|
requestType: options.requestType,
|
|
423
445
|
query: options.query.substring(0, 50),
|
|
424
446
|
});
|
|
@@ -429,9 +451,28 @@ export class AxonFlow {
|
|
|
429
451
|
body: JSON.stringify(agentRequest),
|
|
430
452
|
signal: AbortSignal.timeout(this.config.timeout),
|
|
431
453
|
});
|
|
454
|
+
let data;
|
|
432
455
|
if (!response.ok) {
|
|
433
456
|
const errorText = await response.text();
|
|
434
|
-
|
|
457
|
+
// Handle HTTP 402 (Payment Required) for budget exceeded - parse as blocked response with budgetInfo
|
|
458
|
+
if (response.status === 402) {
|
|
459
|
+
try {
|
|
460
|
+
data = JSON.parse(errorText);
|
|
461
|
+
// If it has budget_info, treat as valid blocked response (fall through to normal processing)
|
|
462
|
+
if (data.budget_info) {
|
|
463
|
+
// Fall through to normal response processing below
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
throw new APIError(response.status, 'Payment Required', errorText);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
if (e instanceof APIError)
|
|
471
|
+
throw e;
|
|
472
|
+
throw new APIError(response.status, 'Payment Required', errorText);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else if (response.status === 401 || response.status === 403) {
|
|
435
476
|
// Try to parse as JSON for policy violation info
|
|
436
477
|
try {
|
|
437
478
|
const errorJson = JSON.parse(errorText);
|
|
@@ -445,11 +486,17 @@ export class AxonFlow {
|
|
|
445
486
|
}
|
|
446
487
|
throw new AuthenticationError(`Request failed: ${errorText}`);
|
|
447
488
|
}
|
|
448
|
-
|
|
489
|
+
else {
|
|
490
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Parse response if not already parsed (from 402 handling)
|
|
494
|
+
if (!data) {
|
|
495
|
+
data = await response.json();
|
|
449
496
|
}
|
|
450
|
-
const data = await response.json();
|
|
451
497
|
// Check for policy violation in successful response (some blocked responses return 200)
|
|
452
|
-
|
|
498
|
+
// Note: Don't throw for budget blocks (402 responses) - return with budgetInfo instead
|
|
499
|
+
if (data.blocked && !data.budget_info) {
|
|
453
500
|
throw new PolicyViolationError(data.block_reason || 'Request blocked by policy', data.policy_info?.policies_evaluated);
|
|
454
501
|
}
|
|
455
502
|
// Transform snake_case response to camelCase
|
|
@@ -474,8 +521,20 @@ export class AxonFlow {
|
|
|
474
521
|
codeArtifact: data.policy_info.code_artifact,
|
|
475
522
|
};
|
|
476
523
|
}
|
|
524
|
+
// Parse budget info if present (Issue #1082)
|
|
525
|
+
if (data.budget_info) {
|
|
526
|
+
result.budgetInfo = {
|
|
527
|
+
budgetId: data.budget_info.budget_id,
|
|
528
|
+
budgetName: data.budget_info.budget_name,
|
|
529
|
+
usedUsd: data.budget_info.used_usd || 0,
|
|
530
|
+
limitUsd: data.budget_info.limit_usd || 0,
|
|
531
|
+
percentage: data.budget_info.percentage || 0,
|
|
532
|
+
exceeded: data.budget_info.exceeded || false,
|
|
533
|
+
action: data.budget_info.action,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
477
536
|
if (this.config.debug) {
|
|
478
|
-
debugLog('Proxy Mode:
|
|
537
|
+
debugLog('Proxy Mode: proxyLLMCall result', {
|
|
479
538
|
success: result.success,
|
|
480
539
|
blocked: result.blocked,
|
|
481
540
|
hasData: !!result.data,
|
|
@@ -575,19 +634,115 @@ export class AxonFlow {
|
|
|
575
634
|
meta: agentResponse.metadata,
|
|
576
635
|
};
|
|
577
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* Execute a query directly against the MCP connector endpoint.
|
|
639
|
+
*
|
|
640
|
+
* This method calls the agent's /mcp/resources/query endpoint which provides:
|
|
641
|
+
* - Request-phase policy evaluation (SQLi blocking, PII blocking)
|
|
642
|
+
* - Response-phase policy evaluation (PII redaction)
|
|
643
|
+
* - PolicyInfo metadata in responses
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* ```typescript
|
|
647
|
+
* const response = await axonflow.mcpQuery({
|
|
648
|
+
* connector: 'postgres',
|
|
649
|
+
* statement: 'SELECT * FROM customers LIMIT 10',
|
|
650
|
+
* });
|
|
651
|
+
*
|
|
652
|
+
* if (response.redacted) {
|
|
653
|
+
* console.log('Fields redacted:', response.redacted_fields);
|
|
654
|
+
* }
|
|
655
|
+
* console.log('Policies evaluated:', response.policy_info?.policies_evaluated);
|
|
656
|
+
* ```
|
|
657
|
+
*
|
|
658
|
+
* @param options - Query options including connector name and SQL statement
|
|
659
|
+
* @returns ConnectorResponse with data, redaction info, and policy_info
|
|
660
|
+
* @throws ConnectorError if the request is blocked by policy or fails
|
|
661
|
+
*/
|
|
662
|
+
async mcpQuery(options) {
|
|
663
|
+
if (!options.connector) {
|
|
664
|
+
throw new ConnectorError('connector name is required', undefined, 'mcpQuery');
|
|
665
|
+
}
|
|
666
|
+
if (!options.statement) {
|
|
667
|
+
throw new ConnectorError('statement is required', undefined, 'mcpQuery');
|
|
668
|
+
}
|
|
669
|
+
const url = `${this.config.endpoint}/mcp/resources/query`;
|
|
670
|
+
const headers = {
|
|
671
|
+
'Content-Type': 'application/json',
|
|
672
|
+
...this.getAuthHeaders(),
|
|
673
|
+
};
|
|
674
|
+
const body = {
|
|
675
|
+
connector: options.connector,
|
|
676
|
+
statement: options.statement,
|
|
677
|
+
options: options.options || {},
|
|
678
|
+
};
|
|
679
|
+
if (this.config.debug) {
|
|
680
|
+
debugLog('MCP Query', {
|
|
681
|
+
connector: options.connector,
|
|
682
|
+
statement: options.statement.substring(0, 50),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
const response = await fetch(url, {
|
|
686
|
+
method: 'POST',
|
|
687
|
+
headers,
|
|
688
|
+
body: JSON.stringify(body),
|
|
689
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
690
|
+
});
|
|
691
|
+
const responseData = await response.json();
|
|
692
|
+
// Handle policy blocks (403 responses)
|
|
693
|
+
if (!response.ok) {
|
|
694
|
+
throw new ConnectorError(responseData.error || `MCP query failed: ${response.status} ${response.statusText}`, options.connector, 'mcpQuery');
|
|
695
|
+
}
|
|
696
|
+
if (this.config.debug) {
|
|
697
|
+
debugLog('MCP Query result', {
|
|
698
|
+
connector: options.connector,
|
|
699
|
+
success: responseData.success,
|
|
700
|
+
redacted: responseData.redacted,
|
|
701
|
+
policiesEvaluated: responseData.policy_info?.policies_evaluated,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
success: responseData.success,
|
|
706
|
+
data: responseData.data,
|
|
707
|
+
error: responseData.error,
|
|
708
|
+
meta: responseData.meta,
|
|
709
|
+
redacted: responseData.redacted,
|
|
710
|
+
redacted_fields: responseData.redacted_fields,
|
|
711
|
+
policy_info: responseData.policy_info,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Execute a statement against an MCP connector (alias for mcpQuery).
|
|
716
|
+
*
|
|
717
|
+
* Same as mcpQuery but follows the naming convention of other execute* methods.
|
|
718
|
+
*
|
|
719
|
+
* @param options - Query options including connector name and SQL statement
|
|
720
|
+
* @returns ConnectorResponse with data, redaction info, and policy_info
|
|
721
|
+
*/
|
|
722
|
+
async mcpExecute(options) {
|
|
723
|
+
return this.mcpQuery(options);
|
|
724
|
+
}
|
|
578
725
|
/**
|
|
579
726
|
* Generate a multi-agent execution plan from a natural language query
|
|
580
727
|
* @param query - Natural language query describing the task
|
|
581
728
|
* @param domain - Optional domain hint (travel, healthcare, etc.)
|
|
582
729
|
* @param userToken - Optional user token for authentication (defaults to tenant/client_id)
|
|
730
|
+
* @param options - Optional plan generation options (execution mode, etc.)
|
|
583
731
|
*/
|
|
584
|
-
async generatePlan(query, domain, userToken) {
|
|
732
|
+
async generatePlan(query, domain, userToken, options) {
|
|
733
|
+
const context = {};
|
|
734
|
+
if (domain) {
|
|
735
|
+
context.domain = domain;
|
|
736
|
+
}
|
|
737
|
+
if (options?.executionMode) {
|
|
738
|
+
context.execution_mode = options.executionMode;
|
|
739
|
+
}
|
|
585
740
|
const agentRequest = {
|
|
586
741
|
query,
|
|
587
|
-
user_token: userToken || this.config.tenant,
|
|
588
|
-
client_id: this.config.tenant,
|
|
742
|
+
user_token: userToken || this.config.clientId || this.config.tenant,
|
|
743
|
+
client_id: this.config.clientId || this.config.tenant,
|
|
589
744
|
request_type: 'multi-agent-plan',
|
|
590
|
-
context
|
|
745
|
+
context,
|
|
591
746
|
};
|
|
592
747
|
const url = `${this.config.endpoint}/api/request`;
|
|
593
748
|
const headers = {
|
|
@@ -631,8 +786,8 @@ export class AxonFlow {
|
|
|
631
786
|
async executePlan(planId, userToken) {
|
|
632
787
|
const agentRequest = {
|
|
633
788
|
query: '',
|
|
634
|
-
user_token: userToken || this.config.tenant,
|
|
635
|
-
client_id: this.config.tenant,
|
|
789
|
+
user_token: userToken || this.config.clientId || this.config.tenant,
|
|
790
|
+
client_id: this.config.clientId || this.config.tenant,
|
|
636
791
|
request_type: 'execute-plan',
|
|
637
792
|
context: { plan_id: planId },
|
|
638
793
|
};
|
|
@@ -653,16 +808,43 @@ export class AxonFlow {
|
|
|
653
808
|
throw new PlanExecutionError(`Plan execution failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'execution');
|
|
654
809
|
}
|
|
655
810
|
const agentResponse = await response.json();
|
|
811
|
+
// Detect nested data.success=false (agent wraps orchestrator errors)
|
|
812
|
+
let success = agentResponse.success;
|
|
813
|
+
let error = agentResponse.error;
|
|
814
|
+
let result = agentResponse.result;
|
|
815
|
+
const data = agentResponse.data;
|
|
816
|
+
if (data && typeof data === 'object' && data.success === false) {
|
|
817
|
+
success = false;
|
|
818
|
+
if (data.error && !error)
|
|
819
|
+
error = data.error;
|
|
820
|
+
// Throw on nested failure (e.g., cancelled plan execution)
|
|
821
|
+
throw new PlanExecutionError(error || 'Plan execution failed', planId, 'execution');
|
|
822
|
+
}
|
|
823
|
+
if (!result && data?.result)
|
|
824
|
+
result = data.result;
|
|
656
825
|
if (this.config.debug) {
|
|
657
|
-
debugLog('Plan executed', { planId, success
|
|
826
|
+
debugLog('Plan executed', { planId, success });
|
|
827
|
+
}
|
|
828
|
+
// Read status from response data if available (e.g., "awaiting_approval" for confirm mode)
|
|
829
|
+
let status = success ? 'completed' : 'failed';
|
|
830
|
+
if (data &&
|
|
831
|
+
typeof data === 'object' &&
|
|
832
|
+
'status' in data &&
|
|
833
|
+
typeof data.status === 'string' &&
|
|
834
|
+
data.status) {
|
|
835
|
+
status = data.status;
|
|
836
|
+
}
|
|
837
|
+
else if (agentResponse.metadata?.status) {
|
|
838
|
+
status = agentResponse.metadata.status;
|
|
658
839
|
}
|
|
659
840
|
return {
|
|
660
841
|
planId,
|
|
661
|
-
status
|
|
662
|
-
result
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
842
|
+
status,
|
|
843
|
+
result,
|
|
844
|
+
workflowId: data?.workflow_id,
|
|
845
|
+
stepResults: agentResponse.metadata?.step_results ?? data?.metadata?.step_results,
|
|
846
|
+
error,
|
|
847
|
+
duration: agentResponse.metadata?.duration ?? data?.metadata?.duration,
|
|
666
848
|
};
|
|
667
849
|
}
|
|
668
850
|
/**
|
|
@@ -688,6 +870,150 @@ export class AxonFlow {
|
|
|
688
870
|
duration: status.duration,
|
|
689
871
|
};
|
|
690
872
|
}
|
|
873
|
+
/**
|
|
874
|
+
* Cancel a running or pending plan
|
|
875
|
+
* @param planId - ID of the plan to cancel
|
|
876
|
+
* @param reason - Optional reason for cancellation
|
|
877
|
+
*/
|
|
878
|
+
async cancelPlan(planId, reason) {
|
|
879
|
+
const url = `${this.config.endpoint}/api/v1/plan/${planId}/cancel`;
|
|
880
|
+
const headers = {
|
|
881
|
+
'Content-Type': 'application/json',
|
|
882
|
+
...this.getAuthHeaders(),
|
|
883
|
+
};
|
|
884
|
+
const body = {};
|
|
885
|
+
if (reason) {
|
|
886
|
+
body.reason = reason;
|
|
887
|
+
}
|
|
888
|
+
const response = await fetch(url, {
|
|
889
|
+
method: 'POST',
|
|
890
|
+
headers,
|
|
891
|
+
body: JSON.stringify(body),
|
|
892
|
+
signal: AbortSignal.timeout(this.config.mapTimeout),
|
|
893
|
+
});
|
|
894
|
+
if (!response.ok) {
|
|
895
|
+
const errorText = await response.text();
|
|
896
|
+
throw new PlanExecutionError(`Plan cancellation failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'cancel');
|
|
897
|
+
}
|
|
898
|
+
const data = await response.json();
|
|
899
|
+
if (this.config.debug) {
|
|
900
|
+
debugLog('Plan cancelled', { planId, status: data.status });
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
planId: data.plan_id || planId,
|
|
904
|
+
status: data.status,
|
|
905
|
+
message: data.message,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Update a plan with optimistic concurrency control.
|
|
910
|
+
* Throws VersionConflictError on 409 (version mismatch).
|
|
911
|
+
* @param planId - ID of the plan to update
|
|
912
|
+
* @param request - Update request with version and fields to change
|
|
913
|
+
*/
|
|
914
|
+
async updatePlan(planId, request) {
|
|
915
|
+
const url = `${this.config.endpoint}/api/v1/plan/${planId}`;
|
|
916
|
+
const headers = {
|
|
917
|
+
'Content-Type': 'application/json',
|
|
918
|
+
...this.getAuthHeaders(),
|
|
919
|
+
};
|
|
920
|
+
const body = {
|
|
921
|
+
version: request.version,
|
|
922
|
+
};
|
|
923
|
+
if (request.executionMode) {
|
|
924
|
+
body.execution_mode = request.executionMode;
|
|
925
|
+
}
|
|
926
|
+
if (request.domain) {
|
|
927
|
+
body.domain = request.domain;
|
|
928
|
+
}
|
|
929
|
+
const response = await fetch(url, {
|
|
930
|
+
method: 'PUT',
|
|
931
|
+
headers,
|
|
932
|
+
body: JSON.stringify(body),
|
|
933
|
+
signal: AbortSignal.timeout(this.config.mapTimeout),
|
|
934
|
+
});
|
|
935
|
+
if (response.status === 409) {
|
|
936
|
+
const errorData = await response.json().catch(() => ({}));
|
|
937
|
+
throw new VersionConflictError(planId, request.version, errorData.current_version);
|
|
938
|
+
}
|
|
939
|
+
if (!response.ok) {
|
|
940
|
+
const errorText = await response.text();
|
|
941
|
+
throw new PlanExecutionError(`Plan update failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'update');
|
|
942
|
+
}
|
|
943
|
+
const data = await response.json();
|
|
944
|
+
if (this.config.debug) {
|
|
945
|
+
debugLog('Plan updated', { planId, version: data.version });
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
planId: data.plan_id || planId,
|
|
949
|
+
version: data.version,
|
|
950
|
+
status: data.status,
|
|
951
|
+
success: data.success ?? true,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Get version history for a plan
|
|
956
|
+
* @param planId - ID of the plan
|
|
957
|
+
*/
|
|
958
|
+
async getPlanVersions(planId) {
|
|
959
|
+
const url = `${this.config.endpoint}/api/v1/plan/${planId}/versions`;
|
|
960
|
+
const headers = {
|
|
961
|
+
...this.getAuthHeaders(),
|
|
962
|
+
};
|
|
963
|
+
const response = await fetch(url, {
|
|
964
|
+
method: 'GET',
|
|
965
|
+
headers,
|
|
966
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
967
|
+
});
|
|
968
|
+
if (!response.ok) {
|
|
969
|
+
const errorText = await response.text();
|
|
970
|
+
throw new PlanExecutionError(`Get plan versions failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'versions');
|
|
971
|
+
}
|
|
972
|
+
const data = await response.json();
|
|
973
|
+
const versions = (data.versions || []).map((v) => ({
|
|
974
|
+
version: v.version,
|
|
975
|
+
changedAt: v.changed_at,
|
|
976
|
+
changedBy: v.changed_by,
|
|
977
|
+
changeType: v.change_type,
|
|
978
|
+
changeSummary: v.change_summary,
|
|
979
|
+
}));
|
|
980
|
+
return {
|
|
981
|
+
planId: data.plan_id || planId,
|
|
982
|
+
versions,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Resume a paused plan (e.g., after approval gate or confirm mode)
|
|
987
|
+
* @param planId - ID of the plan to resume
|
|
988
|
+
* @param approved - Whether the plan is approved to proceed (defaults to true)
|
|
989
|
+
*/
|
|
990
|
+
async resumePlan(planId, approved) {
|
|
991
|
+
const url = `${this.config.endpoint}/api/v1/plan/${planId}/resume`;
|
|
992
|
+
const headers = {
|
|
993
|
+
'Content-Type': 'application/json',
|
|
994
|
+
...this.getAuthHeaders(),
|
|
995
|
+
};
|
|
996
|
+
const response = await fetch(url, {
|
|
997
|
+
method: 'POST',
|
|
998
|
+
headers,
|
|
999
|
+
body: JSON.stringify({ approved: approved ?? true }),
|
|
1000
|
+
signal: AbortSignal.timeout(this.config.mapTimeout),
|
|
1001
|
+
});
|
|
1002
|
+
if (!response.ok) {
|
|
1003
|
+
const errorText = await response.text();
|
|
1004
|
+
throw new PlanExecutionError(`Plan resume failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'resume');
|
|
1005
|
+
}
|
|
1006
|
+
const data = await response.json();
|
|
1007
|
+
if (this.config.debug) {
|
|
1008
|
+
debugLog('Plan resumed', { planId, approved: data.approved });
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
planId: data.plan_id || planId,
|
|
1012
|
+
status: data.status,
|
|
1013
|
+
approved: data.approved,
|
|
1014
|
+
message: data.message,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
691
1017
|
// ============================================================================
|
|
692
1018
|
// Gateway Mode Methods
|
|
693
1019
|
// ============================================================================
|
|
@@ -740,12 +1066,12 @@ export class AxonFlow {
|
|
|
740
1066
|
* ```
|
|
741
1067
|
*/
|
|
742
1068
|
async getPolicyApprovedContext(options) {
|
|
743
|
-
//
|
|
744
|
-
|
|
1069
|
+
// Use smart default for clientId - enables zero-config community mode
|
|
1070
|
+
const clientId = this.getEffectiveClientId();
|
|
745
1071
|
const url = `${this.config.endpoint}/api/policy/pre-check`;
|
|
746
1072
|
const requestBody = {
|
|
747
1073
|
user_token: options.userToken,
|
|
748
|
-
client_id:
|
|
1074
|
+
client_id: clientId,
|
|
749
1075
|
query: options.query,
|
|
750
1076
|
data_sources: options.dataSources || [],
|
|
751
1077
|
context: options.context || {},
|
|
@@ -825,12 +1151,12 @@ export class AxonFlow {
|
|
|
825
1151
|
* ```
|
|
826
1152
|
*/
|
|
827
1153
|
async auditLLMCall(options) {
|
|
828
|
-
//
|
|
829
|
-
|
|
1154
|
+
// Use smart default for clientId - enables zero-config community mode
|
|
1155
|
+
const clientId = this.getEffectiveClientId();
|
|
830
1156
|
const url = `${this.config.endpoint}/api/audit/llm-call`;
|
|
831
1157
|
const requestBody = {
|
|
832
1158
|
context_id: options.contextId,
|
|
833
|
-
client_id:
|
|
1159
|
+
client_id: clientId,
|
|
834
1160
|
response_summary: options.responseSummary,
|
|
835
1161
|
provider: options.provider,
|
|
836
1162
|
model: options.model,
|
|
@@ -1036,11 +1362,9 @@ export class AxonFlow {
|
|
|
1036
1362
|
'Content-Type': 'application/json',
|
|
1037
1363
|
...this.getAuthHeaders(),
|
|
1038
1364
|
};
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
|
|
1042
|
-
headers['X-Org-ID'] = this.config.tenant;
|
|
1043
|
-
}
|
|
1365
|
+
// Note: X-Tenant-ID is set by getAuthHeaders() from clientId
|
|
1366
|
+
// Do NOT set X-Org-ID here - the server derives org from tenant context
|
|
1367
|
+
// Setting X-Org-ID to 'default' breaks budget queries which expect org_id to match client.OrgID
|
|
1044
1368
|
return headers;
|
|
1045
1369
|
}
|
|
1046
1370
|
/**
|
|
@@ -1400,6 +1724,10 @@ export class AxonFlow {
|
|
|
1400
1724
|
const params = new URLSearchParams();
|
|
1401
1725
|
if (options?.type)
|
|
1402
1726
|
params.set('type', options.type);
|
|
1727
|
+
if (options?.tier)
|
|
1728
|
+
params.set('tier', options.tier);
|
|
1729
|
+
if (options?.organizationId)
|
|
1730
|
+
params.set('organization_id', options.organizationId);
|
|
1403
1731
|
if (options?.enabled !== undefined)
|
|
1404
1732
|
params.set('enabled', String(options.enabled));
|
|
1405
1733
|
if (options?.limit)
|
|
@@ -1460,8 +1788,27 @@ export class AxonFlow {
|
|
|
1460
1788
|
if (this.config.debug) {
|
|
1461
1789
|
debugLog('Creating dynamic policy', { name: policy.name });
|
|
1462
1790
|
}
|
|
1791
|
+
// Convert camelCase to snake_case for API compatibility
|
|
1792
|
+
const requestBody = {
|
|
1793
|
+
name: policy.name,
|
|
1794
|
+
type: policy.type,
|
|
1795
|
+
conditions: policy.conditions,
|
|
1796
|
+
actions: policy.actions,
|
|
1797
|
+
};
|
|
1798
|
+
if (policy.description)
|
|
1799
|
+
requestBody.description = policy.description;
|
|
1800
|
+
if (policy.category)
|
|
1801
|
+
requestBody.category = policy.category;
|
|
1802
|
+
if (policy.priority !== undefined)
|
|
1803
|
+
requestBody.priority = policy.priority;
|
|
1804
|
+
if (policy.enabled !== undefined)
|
|
1805
|
+
requestBody.enabled = policy.enabled;
|
|
1806
|
+
requestBody.tier = policy.tier || 'tenant';
|
|
1807
|
+
if (policy.organizationId) {
|
|
1808
|
+
requestBody.organization_id = policy.organizationId;
|
|
1809
|
+
}
|
|
1463
1810
|
// API returns {"policy": {...}} wrapper via Agent proxy
|
|
1464
|
-
const response = await this.orchestratorRequest('POST', '/api/v1/dynamic-policies',
|
|
1811
|
+
const response = await this.orchestratorRequest('POST', '/api/v1/dynamic-policies', requestBody);
|
|
1465
1812
|
// Handle both wrapped and unwrapped responses for compatibility
|
|
1466
1813
|
return 'policy' in response ? response.policy : response;
|
|
1467
1814
|
}
|
|
@@ -1476,8 +1823,30 @@ export class AxonFlow {
|
|
|
1476
1823
|
if (this.config.debug) {
|
|
1477
1824
|
debugLog('Updating dynamic policy', { id, updates: Object.keys(policy) });
|
|
1478
1825
|
}
|
|
1826
|
+
// Convert camelCase to snake_case for API compatibility
|
|
1827
|
+
const requestBody = {};
|
|
1828
|
+
if (policy.name !== undefined)
|
|
1829
|
+
requestBody.name = policy.name;
|
|
1830
|
+
if (policy.description !== undefined)
|
|
1831
|
+
requestBody.description = policy.description;
|
|
1832
|
+
if (policy.type !== undefined)
|
|
1833
|
+
requestBody.type = policy.type;
|
|
1834
|
+
if (policy.category !== undefined)
|
|
1835
|
+
requestBody.category = policy.category;
|
|
1836
|
+
if (policy.tier !== undefined)
|
|
1837
|
+
requestBody.tier = policy.tier;
|
|
1838
|
+
if (policy.organizationId !== undefined)
|
|
1839
|
+
requestBody.organization_id = policy.organizationId;
|
|
1840
|
+
if (policy.conditions !== undefined)
|
|
1841
|
+
requestBody.conditions = policy.conditions;
|
|
1842
|
+
if (policy.actions !== undefined)
|
|
1843
|
+
requestBody.actions = policy.actions;
|
|
1844
|
+
if (policy.priority !== undefined)
|
|
1845
|
+
requestBody.priority = policy.priority;
|
|
1846
|
+
if (policy.enabled !== undefined)
|
|
1847
|
+
requestBody.enabled = policy.enabled;
|
|
1479
1848
|
// API returns {"policy": {...}} wrapper via Agent proxy
|
|
1480
|
-
const response = await this.orchestratorRequest('PUT', `/api/v1/dynamic-policies/${id}`,
|
|
1849
|
+
const response = await this.orchestratorRequest('PUT', `/api/v1/dynamic-policies/${id}`, requestBody);
|
|
1481
1850
|
// Handle both wrapped and unwrapped responses for compatibility
|
|
1482
1851
|
return 'policy' in response ? response.policy : response;
|
|
1483
1852
|
}
|
|
@@ -2786,5 +3155,1351 @@ export class AxonFlow {
|
|
|
2786
3155
|
}
|
|
2787
3156
|
return response.text();
|
|
2788
3157
|
}
|
|
3158
|
+
// =============================================================================
|
|
3159
|
+
// Workflow Control Plane (Issue #834)
|
|
3160
|
+
// =============================================================================
|
|
3161
|
+
/**
|
|
3162
|
+
* Create a new workflow for governance tracking.
|
|
3163
|
+
*
|
|
3164
|
+
* Call this at the start of your external orchestrator workflow (LangChain, LangGraph, CrewAI, etc.)
|
|
3165
|
+
* to register it with AxonFlow for governance tracking.
|
|
3166
|
+
*
|
|
3167
|
+
* @example
|
|
3168
|
+
* ```typescript
|
|
3169
|
+
* const workflow = await client.createWorkflow({
|
|
3170
|
+
* workflow_name: 'customer-support-agent',
|
|
3171
|
+
* source: 'langgraph',
|
|
3172
|
+
* total_steps: 5,
|
|
3173
|
+
* metadata: { customer_id: 'cust-123' }
|
|
3174
|
+
* });
|
|
3175
|
+
* console.log(`Workflow created: ${workflow.workflow_id}`);
|
|
3176
|
+
* ```
|
|
3177
|
+
*/
|
|
3178
|
+
async createWorkflow(request) {
|
|
3179
|
+
const response = await this.orchestratorRequest('POST', '/api/v1/workflows', request);
|
|
3180
|
+
return response;
|
|
3181
|
+
}
|
|
3182
|
+
/**
|
|
3183
|
+
* Get the status of a workflow.
|
|
3184
|
+
*
|
|
3185
|
+
* @example
|
|
3186
|
+
* ```typescript
|
|
3187
|
+
* const status = await client.getWorkflow('wf_123');
|
|
3188
|
+
* console.log(`Status: ${status.status}, Step: ${status.current_step_index}`);
|
|
3189
|
+
* ```
|
|
3190
|
+
*/
|
|
3191
|
+
async getWorkflow(workflowId) {
|
|
3192
|
+
if (!workflowId) {
|
|
3193
|
+
throw new ConfigurationError('Workflow ID is required');
|
|
3194
|
+
}
|
|
3195
|
+
const response = await this.orchestratorRequest('GET', `/api/v1/workflows/${workflowId}`);
|
|
3196
|
+
return response;
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Check if a workflow step is allowed to proceed (step gate).
|
|
3200
|
+
*
|
|
3201
|
+
* This is the core governance method. Call this before executing each step
|
|
3202
|
+
* in your workflow to check if the step is allowed based on policies.
|
|
3203
|
+
*
|
|
3204
|
+
* @example
|
|
3205
|
+
* ```typescript
|
|
3206
|
+
* const gate = await client.stepGate('wf_123', 'step-generate-code', {
|
|
3207
|
+
* step_name: 'Generate Code',
|
|
3208
|
+
* step_type: 'llm_call',
|
|
3209
|
+
* model: 'gpt-4',
|
|
3210
|
+
* provider: 'openai',
|
|
3211
|
+
* step_input: { prompt: 'Generate a hello world function' }
|
|
3212
|
+
* });
|
|
3213
|
+
*
|
|
3214
|
+
* if (gate.decision === 'block') {
|
|
3215
|
+
* throw new Error(`Step blocked: ${gate.reason}`);
|
|
3216
|
+
* }
|
|
3217
|
+
* if (gate.decision === 'require_approval') {
|
|
3218
|
+
* console.log(`Approval required: ${gate.approval_url}`);
|
|
3219
|
+
* return;
|
|
3220
|
+
* }
|
|
3221
|
+
* // Step is allowed, proceed with execution
|
|
3222
|
+
* ```
|
|
3223
|
+
*/
|
|
3224
|
+
async stepGate(workflowId, stepId, request) {
|
|
3225
|
+
if (!workflowId) {
|
|
3226
|
+
throw new ConfigurationError('Workflow ID is required');
|
|
3227
|
+
}
|
|
3228
|
+
if (!stepId) {
|
|
3229
|
+
throw new ConfigurationError('Step ID is required');
|
|
3230
|
+
}
|
|
3231
|
+
const response = await this.orchestratorRequest('POST', `/api/v1/workflows/${workflowId}/steps/${stepId}/gate`, request);
|
|
3232
|
+
return response;
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* Complete a workflow successfully.
|
|
3236
|
+
*
|
|
3237
|
+
* Call this when your workflow has completed all steps successfully.
|
|
3238
|
+
*
|
|
3239
|
+
* @example
|
|
3240
|
+
* ```typescript
|
|
3241
|
+
* await client.completeWorkflow('wf_123');
|
|
3242
|
+
* ```
|
|
3243
|
+
*/
|
|
3244
|
+
async completeWorkflow(workflowId) {
|
|
3245
|
+
if (!workflowId) {
|
|
3246
|
+
throw new ConfigurationError('Workflow ID is required');
|
|
3247
|
+
}
|
|
3248
|
+
await this.orchestratorRequest('POST', `/api/v1/workflows/${workflowId}/complete`, {});
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* Abort a workflow.
|
|
3252
|
+
*
|
|
3253
|
+
* Call this when you need to stop a workflow due to an error or user request.
|
|
3254
|
+
*
|
|
3255
|
+
* @example
|
|
3256
|
+
* ```typescript
|
|
3257
|
+
* await client.abortWorkflow('wf_123', 'User cancelled the operation');
|
|
3258
|
+
* ```
|
|
3259
|
+
*/
|
|
3260
|
+
async abortWorkflow(workflowId, reason) {
|
|
3261
|
+
if (!workflowId) {
|
|
3262
|
+
throw new ConfigurationError('Workflow ID is required');
|
|
3263
|
+
}
|
|
3264
|
+
const request = reason ? { reason } : {};
|
|
3265
|
+
await this.orchestratorRequest('POST', `/api/v1/workflows/${workflowId}/abort`, request);
|
|
3266
|
+
}
|
|
3267
|
+
/**
|
|
3268
|
+
* Mark a workflow step as completed.
|
|
3269
|
+
*
|
|
3270
|
+
* Call this after a step has been executed successfully.
|
|
3271
|
+
*
|
|
3272
|
+
* @example
|
|
3273
|
+
* ```typescript
|
|
3274
|
+
* await client.markStepCompleted('wf_123', 'step-1', {
|
|
3275
|
+
* output: { result: 'success' }
|
|
3276
|
+
* });
|
|
3277
|
+
* ```
|
|
3278
|
+
*/
|
|
3279
|
+
async markStepCompleted(workflowId, stepId, request) {
|
|
3280
|
+
if (!workflowId) {
|
|
3281
|
+
throw new ConfigurationError('Workflow ID is required');
|
|
3282
|
+
}
|
|
3283
|
+
if (!stepId) {
|
|
3284
|
+
throw new ConfigurationError('Step ID is required');
|
|
3285
|
+
}
|
|
3286
|
+
await this.orchestratorRequest('POST', `/api/v1/workflows/${workflowId}/steps/${stepId}/complete`, request || {});
|
|
3287
|
+
}
|
|
3288
|
+
/**
|
|
3289
|
+
* Resume a workflow after approval.
|
|
3290
|
+
*
|
|
3291
|
+
* Call this after a step has been approved to continue the workflow.
|
|
3292
|
+
*
|
|
3293
|
+
* @example
|
|
3294
|
+
* ```typescript
|
|
3295
|
+
* // After approval received via webhook or polling
|
|
3296
|
+
* await client.resumeWorkflow('wf_123');
|
|
3297
|
+
* ```
|
|
3298
|
+
*/
|
|
3299
|
+
async resumeWorkflow(workflowId) {
|
|
3300
|
+
if (!workflowId) {
|
|
3301
|
+
throw new ConfigurationError('Workflow ID is required');
|
|
3302
|
+
}
|
|
3303
|
+
await this.orchestratorRequest('POST', `/api/v1/workflows/${workflowId}/resume`, {});
|
|
3304
|
+
}
|
|
3305
|
+
/**
|
|
3306
|
+
* List workflows with optional filters.
|
|
3307
|
+
*
|
|
3308
|
+
* @example
|
|
3309
|
+
* ```typescript
|
|
3310
|
+
* const result = await client.listWorkflows({
|
|
3311
|
+
* status: 'in_progress',
|
|
3312
|
+
* source: 'langgraph',
|
|
3313
|
+
* limit: 10
|
|
3314
|
+
* });
|
|
3315
|
+
* console.log(`Found ${result.total} workflows`);
|
|
3316
|
+
* ```
|
|
3317
|
+
*/
|
|
3318
|
+
async listWorkflows(options) {
|
|
3319
|
+
const params = new URLSearchParams();
|
|
3320
|
+
if (options?.status) {
|
|
3321
|
+
params.set('status', options.status);
|
|
3322
|
+
}
|
|
3323
|
+
if (options?.source) {
|
|
3324
|
+
params.set('source', options.source);
|
|
3325
|
+
}
|
|
3326
|
+
if (options?.limit !== undefined) {
|
|
3327
|
+
params.set('limit', options.limit.toString());
|
|
3328
|
+
}
|
|
3329
|
+
if (options?.offset !== undefined) {
|
|
3330
|
+
params.set('offset', options.offset.toString());
|
|
3331
|
+
}
|
|
3332
|
+
const queryString = params.toString();
|
|
3333
|
+
const path = queryString ? `/api/v1/workflows?${queryString}` : '/api/v1/workflows';
|
|
3334
|
+
const response = await this.orchestratorRequest('GET', path);
|
|
3335
|
+
return response;
|
|
3336
|
+
}
|
|
3337
|
+
// =============================================================================
|
|
3338
|
+
// WCP Approval Methods (Feature 5)
|
|
3339
|
+
// =============================================================================
|
|
3340
|
+
/**
|
|
3341
|
+
* Approve a workflow step that requires human approval.
|
|
3342
|
+
*
|
|
3343
|
+
* Call this to approve a step that was gated with a 'require_approval' decision.
|
|
3344
|
+
*
|
|
3345
|
+
* @param workflowId - ID of the workflow
|
|
3346
|
+
* @param stepId - ID of the step to approve
|
|
3347
|
+
* @returns Approval response with status
|
|
3348
|
+
*
|
|
3349
|
+
* @example
|
|
3350
|
+
* ```typescript
|
|
3351
|
+
* const result = await client.approveStep('wf_123', 'step_456');
|
|
3352
|
+
* console.log(`Step ${result.step_id} status: ${result.status}`);
|
|
3353
|
+
* ```
|
|
3354
|
+
*/
|
|
3355
|
+
async approveStep(workflowId, stepId) {
|
|
3356
|
+
if (!workflowId) {
|
|
3357
|
+
throw new ConfigurationError('Workflow ID is required');
|
|
3358
|
+
}
|
|
3359
|
+
if (!stepId) {
|
|
3360
|
+
throw new ConfigurationError('Step ID is required');
|
|
3361
|
+
}
|
|
3362
|
+
return this.orchestratorRequest('POST', `/api/v1/workflow-control/${workflowId}/steps/${stepId}/approve`, {});
|
|
3363
|
+
}
|
|
3364
|
+
/**
|
|
3365
|
+
* Reject a workflow step that requires human approval.
|
|
3366
|
+
*
|
|
3367
|
+
* Call this to reject a step that was gated with a 'require_approval' decision.
|
|
3368
|
+
*
|
|
3369
|
+
* @param workflowId - ID of the workflow
|
|
3370
|
+
* @param stepId - ID of the step to reject
|
|
3371
|
+
* @param reason - Optional reason for rejection
|
|
3372
|
+
* @returns Rejection response with status
|
|
3373
|
+
*
|
|
3374
|
+
* @example
|
|
3375
|
+
* ```typescript
|
|
3376
|
+
* const result = await client.rejectStep('wf_123', 'step_456', 'Policy violation detected');
|
|
3377
|
+
* console.log(`Step ${result.step_id} status: ${result.status}`);
|
|
3378
|
+
* ```
|
|
3379
|
+
*/
|
|
3380
|
+
async rejectStep(workflowId, stepId, reason) {
|
|
3381
|
+
if (!workflowId) {
|
|
3382
|
+
throw new ConfigurationError('Workflow ID is required');
|
|
3383
|
+
}
|
|
3384
|
+
if (!stepId) {
|
|
3385
|
+
throw new ConfigurationError('Step ID is required');
|
|
3386
|
+
}
|
|
3387
|
+
const body = {};
|
|
3388
|
+
if (reason) {
|
|
3389
|
+
body.reason = reason;
|
|
3390
|
+
}
|
|
3391
|
+
return this.orchestratorRequest('POST', `/api/v1/workflow-control/${workflowId}/steps/${stepId}/reject`, body);
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Get pending approvals for workflow steps.
|
|
3395
|
+
*
|
|
3396
|
+
* Lists all steps that are waiting for human approval across all workflows.
|
|
3397
|
+
*
|
|
3398
|
+
* @param options - Optional filtering options
|
|
3399
|
+
* @returns List of pending approvals with total count
|
|
3400
|
+
*
|
|
3401
|
+
* @example
|
|
3402
|
+
* ```typescript
|
|
3403
|
+
* const pending = await client.getPendingApprovals({ limit: 10 });
|
|
3404
|
+
* console.log(`${pending.total} approvals pending`);
|
|
3405
|
+
* for (const approval of pending.approvals) {
|
|
3406
|
+
* console.log(`${approval.workflow_name} / ${approval.step_name}`);
|
|
3407
|
+
* }
|
|
3408
|
+
* ```
|
|
3409
|
+
*/
|
|
3410
|
+
async getPendingApprovals(options) {
|
|
3411
|
+
const params = new URLSearchParams();
|
|
3412
|
+
if (options?.limit !== undefined) {
|
|
3413
|
+
params.set('limit', options.limit.toString());
|
|
3414
|
+
}
|
|
3415
|
+
const queryString = params.toString();
|
|
3416
|
+
const path = queryString
|
|
3417
|
+
? `/api/v1/workflow-control/pending-approvals?${queryString}`
|
|
3418
|
+
: '/api/v1/workflow-control/pending-approvals';
|
|
3419
|
+
return this.orchestratorRequest('GET', path);
|
|
3420
|
+
}
|
|
3421
|
+
// =============================================================================
|
|
3422
|
+
// Plan Rollback (Feature 7)
|
|
3423
|
+
// =============================================================================
|
|
3424
|
+
/**
|
|
3425
|
+
* Rollback a plan to a previous version.
|
|
3426
|
+
*
|
|
3427
|
+
* @param planId - ID of the plan to rollback
|
|
3428
|
+
* @param targetVersion - Version number to rollback to
|
|
3429
|
+
* @returns Rollback response with version information
|
|
3430
|
+
*
|
|
3431
|
+
* @example
|
|
3432
|
+
* ```typescript
|
|
3433
|
+
* const result = await client.rollbackPlan('plan_123', 2);
|
|
3434
|
+
* console.log(`Rolled back to v${result.version} from v${result.previousVersion}`);
|
|
3435
|
+
* ```
|
|
3436
|
+
*/
|
|
3437
|
+
async rollbackPlan(planId, targetVersion) {
|
|
3438
|
+
const url = `${this.config.endpoint}/api/v1/plan/${planId}/rollback/${targetVersion}`;
|
|
3439
|
+
const headers = {
|
|
3440
|
+
'Content-Type': 'application/json',
|
|
3441
|
+
...this.getAuthHeaders(),
|
|
3442
|
+
};
|
|
3443
|
+
const response = await fetch(url, {
|
|
3444
|
+
method: 'POST',
|
|
3445
|
+
headers,
|
|
3446
|
+
body: JSON.stringify({}),
|
|
3447
|
+
signal: AbortSignal.timeout(this.config.mapTimeout),
|
|
3448
|
+
});
|
|
3449
|
+
if (!response.ok) {
|
|
3450
|
+
const errorText = await response.text();
|
|
3451
|
+
throw new PlanExecutionError(`Plan rollback failed: ${response.status} ${response.statusText} - ${errorText}`, planId, 'rollback');
|
|
3452
|
+
}
|
|
3453
|
+
const data = await response.json();
|
|
3454
|
+
if (this.config.debug) {
|
|
3455
|
+
debugLog('Plan rolled back', { planId, version: data.version });
|
|
3456
|
+
}
|
|
3457
|
+
return {
|
|
3458
|
+
planId: data.plan_id || planId,
|
|
3459
|
+
version: data.version,
|
|
3460
|
+
previousVersion: data.previous_version,
|
|
3461
|
+
status: data.status,
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
// =============================================================================
|
|
3465
|
+
// Webhook CRUD Methods (Feature 7)
|
|
3466
|
+
// =============================================================================
|
|
3467
|
+
/**
|
|
3468
|
+
* Create a webhook subscription.
|
|
3469
|
+
*
|
|
3470
|
+
* @param request - Webhook configuration
|
|
3471
|
+
* @returns Created webhook subscription
|
|
3472
|
+
*
|
|
3473
|
+
* @example
|
|
3474
|
+
* ```typescript
|
|
3475
|
+
* const webhook = await client.createWebhook({
|
|
3476
|
+
* url: 'https://example.com/webhook',
|
|
3477
|
+
* events: ['workflow.completed', 'step.approval_required'],
|
|
3478
|
+
* active: true
|
|
3479
|
+
* });
|
|
3480
|
+
* console.log(`Webhook created: ${webhook.id}`);
|
|
3481
|
+
* ```
|
|
3482
|
+
*/
|
|
3483
|
+
async createWebhook(request) {
|
|
3484
|
+
return this.orchestratorRequest('POST', '/api/v1/webhooks', request);
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* Get a webhook subscription by ID.
|
|
3488
|
+
*
|
|
3489
|
+
* @param webhookId - ID of the webhook to retrieve
|
|
3490
|
+
* @returns Webhook subscription details
|
|
3491
|
+
*
|
|
3492
|
+
* @example
|
|
3493
|
+
* ```typescript
|
|
3494
|
+
* const webhook = await client.getWebhook('wh_123');
|
|
3495
|
+
* console.log(`Webhook URL: ${webhook.url}, Active: ${webhook.active}`);
|
|
3496
|
+
* ```
|
|
3497
|
+
*/
|
|
3498
|
+
async getWebhook(webhookId) {
|
|
3499
|
+
if (!webhookId) {
|
|
3500
|
+
throw new ConfigurationError('Webhook ID is required');
|
|
3501
|
+
}
|
|
3502
|
+
return this.orchestratorRequest('GET', `/api/v1/webhooks/${webhookId}`);
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Update a webhook subscription.
|
|
3506
|
+
*
|
|
3507
|
+
* @param webhookId - ID of the webhook to update
|
|
3508
|
+
* @param request - Fields to update
|
|
3509
|
+
* @returns Updated webhook subscription
|
|
3510
|
+
*
|
|
3511
|
+
* @example
|
|
3512
|
+
* ```typescript
|
|
3513
|
+
* const webhook = await client.updateWebhook('wh_123', {
|
|
3514
|
+
* events: ['workflow.completed'],
|
|
3515
|
+
* active: false
|
|
3516
|
+
* });
|
|
3517
|
+
* ```
|
|
3518
|
+
*/
|
|
3519
|
+
async updateWebhook(webhookId, request) {
|
|
3520
|
+
if (!webhookId) {
|
|
3521
|
+
throw new ConfigurationError('Webhook ID is required');
|
|
3522
|
+
}
|
|
3523
|
+
return this.orchestratorRequest('PUT', `/api/v1/webhooks/${webhookId}`, request);
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Delete a webhook subscription.
|
|
3527
|
+
*
|
|
3528
|
+
* @param webhookId - ID of the webhook to delete
|
|
3529
|
+
*
|
|
3530
|
+
* @example
|
|
3531
|
+
* ```typescript
|
|
3532
|
+
* await client.deleteWebhook('wh_123');
|
|
3533
|
+
* ```
|
|
3534
|
+
*/
|
|
3535
|
+
async deleteWebhook(webhookId) {
|
|
3536
|
+
if (!webhookId) {
|
|
3537
|
+
throw new ConfigurationError('Webhook ID is required');
|
|
3538
|
+
}
|
|
3539
|
+
await this.orchestratorRequest('DELETE', `/api/v1/webhooks/${webhookId}`);
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* List all webhook subscriptions.
|
|
3543
|
+
*
|
|
3544
|
+
* @returns List of webhook subscriptions with total count
|
|
3545
|
+
*
|
|
3546
|
+
* @example
|
|
3547
|
+
* ```typescript
|
|
3548
|
+
* const result = await client.listWebhooks();
|
|
3549
|
+
* console.log(`${result.total} webhooks configured`);
|
|
3550
|
+
* for (const wh of result.webhooks) {
|
|
3551
|
+
* console.log(`${wh.id}: ${wh.url} (${wh.active ? 'active' : 'inactive'})`);
|
|
3552
|
+
* }
|
|
3553
|
+
* ```
|
|
3554
|
+
*/
|
|
3555
|
+
async listWebhooks() {
|
|
3556
|
+
return this.orchestratorRequest('GET', '/api/v1/webhooks');
|
|
3557
|
+
}
|
|
3558
|
+
// ===========================================================================
|
|
3559
|
+
// MAS FEAT Compliance Methods (Enterprise)
|
|
3560
|
+
// ===========================================================================
|
|
3561
|
+
/**
|
|
3562
|
+
* MAS FEAT compliance module for Singapore regulatory compliance.
|
|
3563
|
+
*
|
|
3564
|
+
* Enterprise Feature: Requires AxonFlow Enterprise license.
|
|
3565
|
+
*
|
|
3566
|
+
* @example
|
|
3567
|
+
* ```typescript
|
|
3568
|
+
* // Register an AI system
|
|
3569
|
+
* const system = await axonflow.masfeat.registerSystem({
|
|
3570
|
+
* systemId: 'credit-scoring-v1',
|
|
3571
|
+
* systemName: 'Credit Scoring AI',
|
|
3572
|
+
* useCase: 'credit_scoring',
|
|
3573
|
+
* ownerTeam: 'Risk Management',
|
|
3574
|
+
* customerImpact: 4,
|
|
3575
|
+
* modelComplexity: 3,
|
|
3576
|
+
* humanReliance: 5
|
|
3577
|
+
* });
|
|
3578
|
+
*
|
|
3579
|
+
* // Configure kill switch
|
|
3580
|
+
* const ks = await axonflow.masfeat.configureKillSwitch('credit-scoring-v1', {
|
|
3581
|
+
* accuracyThreshold: 0.85,
|
|
3582
|
+
* biasThreshold: 0.15,
|
|
3583
|
+
* autoTriggerEnabled: true
|
|
3584
|
+
* });
|
|
3585
|
+
* ```
|
|
3586
|
+
*/
|
|
3587
|
+
get masfeat() {
|
|
3588
|
+
return {
|
|
3589
|
+
// Registry methods
|
|
3590
|
+
registerSystem: this.masfeatRegisterSystem.bind(this),
|
|
3591
|
+
getSystem: this.masfeatGetSystem.bind(this),
|
|
3592
|
+
updateSystem: this.masfeatUpdateSystem.bind(this),
|
|
3593
|
+
listSystems: this.masfeatListSystems.bind(this),
|
|
3594
|
+
activateSystem: this.masfeatActivateSystem.bind(this),
|
|
3595
|
+
retireSystem: this.masfeatRetireSystem.bind(this),
|
|
3596
|
+
getRegistrySummary: this.masfeatGetRegistrySummary.bind(this),
|
|
3597
|
+
// Assessment methods
|
|
3598
|
+
createAssessment: this.masfeatCreateAssessment.bind(this),
|
|
3599
|
+
getAssessment: this.masfeatGetAssessment.bind(this),
|
|
3600
|
+
updateAssessment: this.masfeatUpdateAssessment.bind(this),
|
|
3601
|
+
listAssessments: this.masfeatListAssessments.bind(this),
|
|
3602
|
+
submitAssessment: this.masfeatSubmitAssessment.bind(this),
|
|
3603
|
+
approveAssessment: this.masfeatApproveAssessment.bind(this),
|
|
3604
|
+
rejectAssessment: this.masfeatRejectAssessment.bind(this),
|
|
3605
|
+
// Kill switch methods
|
|
3606
|
+
getKillSwitch: this.masfeatGetKillSwitch.bind(this),
|
|
3607
|
+
configureKillSwitch: this.masfeatConfigureKillSwitch.bind(this),
|
|
3608
|
+
checkKillSwitch: this.masfeatCheckKillSwitch.bind(this),
|
|
3609
|
+
triggerKillSwitch: this.masfeatTriggerKillSwitch.bind(this),
|
|
3610
|
+
restoreKillSwitch: this.masfeatRestoreKillSwitch.bind(this),
|
|
3611
|
+
enableKillSwitch: this.masfeatEnableKillSwitch.bind(this),
|
|
3612
|
+
disableKillSwitch: this.masfeatDisableKillSwitch.bind(this),
|
|
3613
|
+
getKillSwitchHistory: this.masfeatGetKillSwitchHistory.bind(this),
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
// Registry Methods
|
|
3617
|
+
async masfeatRegisterSystem(request) {
|
|
3618
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/registry`;
|
|
3619
|
+
const body = {
|
|
3620
|
+
system_id: request.systemId,
|
|
3621
|
+
system_name: request.systemName,
|
|
3622
|
+
description: request.description,
|
|
3623
|
+
use_case: request.useCase,
|
|
3624
|
+
owner_team: request.ownerTeam,
|
|
3625
|
+
technical_owner: request.technicalOwner,
|
|
3626
|
+
owner_email: request.businessOwner,
|
|
3627
|
+
risk_rating_impact: request.customerImpact,
|
|
3628
|
+
risk_rating_complexity: request.modelComplexity,
|
|
3629
|
+
risk_rating_reliance: request.humanReliance,
|
|
3630
|
+
metadata: request.metadata,
|
|
3631
|
+
};
|
|
3632
|
+
const response = await fetch(url, {
|
|
3633
|
+
method: 'POST',
|
|
3634
|
+
headers: {
|
|
3635
|
+
'Content-Type': 'application/json',
|
|
3636
|
+
...this.getAuthHeaders(),
|
|
3637
|
+
},
|
|
3638
|
+
body: JSON.stringify(body),
|
|
3639
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3640
|
+
});
|
|
3641
|
+
if (!response.ok) {
|
|
3642
|
+
const errorText = await response.text();
|
|
3643
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3644
|
+
}
|
|
3645
|
+
return this.mapSystemResponse(await response.json());
|
|
3646
|
+
}
|
|
3647
|
+
async masfeatGetSystem(systemId) {
|
|
3648
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/registry/${systemId}`;
|
|
3649
|
+
const response = await fetch(url, {
|
|
3650
|
+
method: 'GET',
|
|
3651
|
+
headers: {
|
|
3652
|
+
...this.getAuthHeaders(),
|
|
3653
|
+
},
|
|
3654
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3655
|
+
});
|
|
3656
|
+
if (!response.ok) {
|
|
3657
|
+
const errorText = await response.text();
|
|
3658
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3659
|
+
}
|
|
3660
|
+
return this.mapSystemResponse(await response.json());
|
|
3661
|
+
}
|
|
3662
|
+
async masfeatUpdateSystem(systemId, request) {
|
|
3663
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/registry/${systemId}`;
|
|
3664
|
+
const body = {};
|
|
3665
|
+
if (request.systemName !== undefined)
|
|
3666
|
+
body.system_name = request.systemName;
|
|
3667
|
+
if (request.description !== undefined)
|
|
3668
|
+
body.description = request.description;
|
|
3669
|
+
if (request.ownerTeam !== undefined)
|
|
3670
|
+
body.owner_team = request.ownerTeam;
|
|
3671
|
+
if (request.technicalOwner !== undefined)
|
|
3672
|
+
body.technical_owner = request.technicalOwner;
|
|
3673
|
+
if (request.businessOwner !== undefined)
|
|
3674
|
+
body.business_owner = request.businessOwner;
|
|
3675
|
+
if (request.customerImpact !== undefined)
|
|
3676
|
+
body.customer_impact = request.customerImpact;
|
|
3677
|
+
if (request.modelComplexity !== undefined)
|
|
3678
|
+
body.model_complexity = request.modelComplexity;
|
|
3679
|
+
if (request.humanReliance !== undefined)
|
|
3680
|
+
body.human_reliance = request.humanReliance;
|
|
3681
|
+
if (request.metadata !== undefined)
|
|
3682
|
+
body.metadata = request.metadata;
|
|
3683
|
+
const response = await fetch(url, {
|
|
3684
|
+
method: 'PUT',
|
|
3685
|
+
headers: {
|
|
3686
|
+
'Content-Type': 'application/json',
|
|
3687
|
+
...this.getAuthHeaders(),
|
|
3688
|
+
},
|
|
3689
|
+
body: JSON.stringify(body),
|
|
3690
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3691
|
+
});
|
|
3692
|
+
if (!response.ok) {
|
|
3693
|
+
const errorText = await response.text();
|
|
3694
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3695
|
+
}
|
|
3696
|
+
return this.mapSystemResponse(await response.json());
|
|
3697
|
+
}
|
|
3698
|
+
async masfeatListSystems(options) {
|
|
3699
|
+
const params = new URLSearchParams();
|
|
3700
|
+
if (options?.status)
|
|
3701
|
+
params.append('status', options.status);
|
|
3702
|
+
if (options?.useCase)
|
|
3703
|
+
params.append('use_case', options.useCase);
|
|
3704
|
+
if (options?.materiality)
|
|
3705
|
+
params.append('materiality', options.materiality);
|
|
3706
|
+
if (options?.limit)
|
|
3707
|
+
params.append('limit', options.limit.toString());
|
|
3708
|
+
if (options?.offset)
|
|
3709
|
+
params.append('offset', options.offset.toString());
|
|
3710
|
+
const queryString = params.toString();
|
|
3711
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/registry${queryString ? `?${queryString}` : ''}`;
|
|
3712
|
+
const response = await fetch(url, {
|
|
3713
|
+
method: 'GET',
|
|
3714
|
+
headers: {
|
|
3715
|
+
...this.getAuthHeaders(),
|
|
3716
|
+
},
|
|
3717
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3718
|
+
});
|
|
3719
|
+
if (!response.ok) {
|
|
3720
|
+
const errorText = await response.text();
|
|
3721
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3722
|
+
}
|
|
3723
|
+
const data = await response.json();
|
|
3724
|
+
return (data || []).map((s) => this.mapSystemResponse(s));
|
|
3725
|
+
}
|
|
3726
|
+
async masfeatActivateSystem(systemId) {
|
|
3727
|
+
// Use PUT to update status - the /activate endpoint doesn't exist
|
|
3728
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/registry/${systemId}`;
|
|
3729
|
+
const response = await fetch(url, {
|
|
3730
|
+
method: 'PUT',
|
|
3731
|
+
headers: {
|
|
3732
|
+
'Content-Type': 'application/json',
|
|
3733
|
+
...this.getAuthHeaders(),
|
|
3734
|
+
},
|
|
3735
|
+
body: JSON.stringify({ status: 'active' }),
|
|
3736
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3737
|
+
});
|
|
3738
|
+
if (!response.ok) {
|
|
3739
|
+
const errorText = await response.text();
|
|
3740
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3741
|
+
}
|
|
3742
|
+
return this.mapSystemResponse(await response.json());
|
|
3743
|
+
}
|
|
3744
|
+
async masfeatRetireSystem(systemId) {
|
|
3745
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/registry/${systemId}`;
|
|
3746
|
+
const response = await fetch(url, {
|
|
3747
|
+
method: 'DELETE',
|
|
3748
|
+
headers: {
|
|
3749
|
+
...this.getAuthHeaders(),
|
|
3750
|
+
},
|
|
3751
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3752
|
+
});
|
|
3753
|
+
if (!response.ok) {
|
|
3754
|
+
const errorText = await response.text();
|
|
3755
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3756
|
+
}
|
|
3757
|
+
return this.mapSystemResponse(await response.json());
|
|
3758
|
+
}
|
|
3759
|
+
async masfeatGetRegistrySummary() {
|
|
3760
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/registry/summary`;
|
|
3761
|
+
const response = await fetch(url, {
|
|
3762
|
+
method: 'GET',
|
|
3763
|
+
headers: {
|
|
3764
|
+
...this.getAuthHeaders(),
|
|
3765
|
+
},
|
|
3766
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3767
|
+
});
|
|
3768
|
+
if (!response.ok) {
|
|
3769
|
+
const errorText = await response.text();
|
|
3770
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3771
|
+
}
|
|
3772
|
+
const data = await response.json();
|
|
3773
|
+
return {
|
|
3774
|
+
totalSystems: data.total_systems,
|
|
3775
|
+
activeSystems: data.active_systems,
|
|
3776
|
+
highMaterialityCount: data.high_materiality_count ?? data.high_materiality ?? 0,
|
|
3777
|
+
mediumMaterialityCount: data.medium_materiality_count ?? data.medium_materiality ?? 0,
|
|
3778
|
+
lowMaterialityCount: data.low_materiality_count ?? data.low_materiality ?? 0,
|
|
3779
|
+
byUseCase: data.by_use_case || {},
|
|
3780
|
+
byStatus: data.by_status || {},
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
// Assessment Methods
|
|
3784
|
+
async masfeatCreateAssessment(request) {
|
|
3785
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/assessments`;
|
|
3786
|
+
const body = {
|
|
3787
|
+
system_id: request.systemId,
|
|
3788
|
+
assessment_type: request.assessmentType || 'periodic',
|
|
3789
|
+
assessors: request.assessors,
|
|
3790
|
+
};
|
|
3791
|
+
if (request.assessmentDate)
|
|
3792
|
+
body.assessment_date = request.assessmentDate.toISOString();
|
|
3793
|
+
if (request.fairnessScore !== undefined)
|
|
3794
|
+
body.fairness_score = request.fairnessScore;
|
|
3795
|
+
if (request.ethicsScore !== undefined)
|
|
3796
|
+
body.ethics_score = request.ethicsScore;
|
|
3797
|
+
if (request.accountabilityScore !== undefined)
|
|
3798
|
+
body.accountability_score = request.accountabilityScore;
|
|
3799
|
+
if (request.transparencyScore !== undefined)
|
|
3800
|
+
body.transparency_score = request.transparencyScore;
|
|
3801
|
+
if (request.fairnessDetails)
|
|
3802
|
+
body.fairness_details = request.fairnessDetails;
|
|
3803
|
+
if (request.ethicsDetails)
|
|
3804
|
+
body.ethics_details = request.ethicsDetails;
|
|
3805
|
+
if (request.accountabilityDetails)
|
|
3806
|
+
body.accountability_details = request.accountabilityDetails;
|
|
3807
|
+
if (request.transparencyDetails)
|
|
3808
|
+
body.transparency_details = request.transparencyDetails;
|
|
3809
|
+
if (request.recommendations)
|
|
3810
|
+
body.recommendations = request.recommendations;
|
|
3811
|
+
if (request.findings) {
|
|
3812
|
+
body.findings = request.findings.map(f => ({
|
|
3813
|
+
id: f.id,
|
|
3814
|
+
pillar: f.pillar,
|
|
3815
|
+
severity: f.severity,
|
|
3816
|
+
category: f.category,
|
|
3817
|
+
description: f.description,
|
|
3818
|
+
status: f.status,
|
|
3819
|
+
remediation: f.remediation,
|
|
3820
|
+
due_date: f.dueDate?.toISOString(),
|
|
3821
|
+
}));
|
|
3822
|
+
}
|
|
3823
|
+
const response = await fetch(url, {
|
|
3824
|
+
method: 'POST',
|
|
3825
|
+
headers: {
|
|
3826
|
+
'Content-Type': 'application/json',
|
|
3827
|
+
...this.getAuthHeaders(),
|
|
3828
|
+
},
|
|
3829
|
+
body: JSON.stringify(body),
|
|
3830
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3831
|
+
});
|
|
3832
|
+
if (!response.ok) {
|
|
3833
|
+
const errorText = await response.text();
|
|
3834
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3835
|
+
}
|
|
3836
|
+
return this.mapAssessmentResponse(await response.json());
|
|
3837
|
+
}
|
|
3838
|
+
async masfeatGetAssessment(assessmentId) {
|
|
3839
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/assessments/${assessmentId}`;
|
|
3840
|
+
const response = await fetch(url, {
|
|
3841
|
+
method: 'GET',
|
|
3842
|
+
headers: {
|
|
3843
|
+
...this.getAuthHeaders(),
|
|
3844
|
+
},
|
|
3845
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3846
|
+
});
|
|
3847
|
+
if (!response.ok) {
|
|
3848
|
+
const errorText = await response.text();
|
|
3849
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3850
|
+
}
|
|
3851
|
+
return this.mapAssessmentResponse(await response.json());
|
|
3852
|
+
}
|
|
3853
|
+
async masfeatUpdateAssessment(assessmentId, request) {
|
|
3854
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/assessments/${assessmentId}`;
|
|
3855
|
+
const body = {};
|
|
3856
|
+
if (request.fairnessScore !== undefined)
|
|
3857
|
+
body.fairness_score = request.fairnessScore;
|
|
3858
|
+
if (request.ethicsScore !== undefined)
|
|
3859
|
+
body.ethics_score = request.ethicsScore;
|
|
3860
|
+
if (request.accountabilityScore !== undefined)
|
|
3861
|
+
body.accountability_score = request.accountabilityScore;
|
|
3862
|
+
if (request.transparencyScore !== undefined)
|
|
3863
|
+
body.transparency_score = request.transparencyScore;
|
|
3864
|
+
if (request.fairnessDetails !== undefined)
|
|
3865
|
+
body.fairness_details = request.fairnessDetails;
|
|
3866
|
+
if (request.ethicsDetails !== undefined)
|
|
3867
|
+
body.ethics_details = request.ethicsDetails;
|
|
3868
|
+
if (request.accountabilityDetails !== undefined)
|
|
3869
|
+
body.accountability_details = request.accountabilityDetails;
|
|
3870
|
+
if (request.transparencyDetails !== undefined)
|
|
3871
|
+
body.transparency_details = request.transparencyDetails;
|
|
3872
|
+
if (request.findings !== undefined) {
|
|
3873
|
+
body.findings = request.findings.map(f => ({
|
|
3874
|
+
id: f.id,
|
|
3875
|
+
pillar: f.pillar,
|
|
3876
|
+
severity: f.severity,
|
|
3877
|
+
category: f.category,
|
|
3878
|
+
description: f.description,
|
|
3879
|
+
status: f.status,
|
|
3880
|
+
remediation: f.remediation,
|
|
3881
|
+
due_date: f.dueDate?.toISOString(),
|
|
3882
|
+
}));
|
|
3883
|
+
}
|
|
3884
|
+
if (request.recommendations !== undefined)
|
|
3885
|
+
body.recommendations = request.recommendations;
|
|
3886
|
+
if (request.assessors !== undefined)
|
|
3887
|
+
body.assessors = request.assessors;
|
|
3888
|
+
const response = await fetch(url, {
|
|
3889
|
+
method: 'PUT',
|
|
3890
|
+
headers: {
|
|
3891
|
+
'Content-Type': 'application/json',
|
|
3892
|
+
...this.getAuthHeaders(),
|
|
3893
|
+
},
|
|
3894
|
+
body: JSON.stringify(body),
|
|
3895
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3896
|
+
});
|
|
3897
|
+
if (!response.ok) {
|
|
3898
|
+
const errorText = await response.text();
|
|
3899
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3900
|
+
}
|
|
3901
|
+
return this.mapAssessmentResponse(await response.json());
|
|
3902
|
+
}
|
|
3903
|
+
async masfeatListAssessments(options) {
|
|
3904
|
+
const params = new URLSearchParams();
|
|
3905
|
+
if (options?.systemId)
|
|
3906
|
+
params.append('system_id', options.systemId);
|
|
3907
|
+
if (options?.status)
|
|
3908
|
+
params.append('status', options.status);
|
|
3909
|
+
if (options?.limit)
|
|
3910
|
+
params.append('limit', options.limit.toString());
|
|
3911
|
+
if (options?.offset)
|
|
3912
|
+
params.append('offset', options.offset.toString());
|
|
3913
|
+
const queryString = params.toString();
|
|
3914
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/assessments${queryString ? `?${queryString}` : ''}`;
|
|
3915
|
+
const response = await fetch(url, {
|
|
3916
|
+
method: 'GET',
|
|
3917
|
+
headers: {
|
|
3918
|
+
...this.getAuthHeaders(),
|
|
3919
|
+
},
|
|
3920
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3921
|
+
});
|
|
3922
|
+
if (!response.ok) {
|
|
3923
|
+
const errorText = await response.text();
|
|
3924
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3925
|
+
}
|
|
3926
|
+
const data = await response.json();
|
|
3927
|
+
return (data || []).map((a) => this.mapAssessmentResponse(a));
|
|
3928
|
+
}
|
|
3929
|
+
async masfeatSubmitAssessment(assessmentId) {
|
|
3930
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/assessments/${assessmentId}/submit`;
|
|
3931
|
+
const response = await fetch(url, {
|
|
3932
|
+
method: 'POST',
|
|
3933
|
+
headers: {
|
|
3934
|
+
'Content-Type': 'application/json',
|
|
3935
|
+
...this.getAuthHeaders(),
|
|
3936
|
+
},
|
|
3937
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3938
|
+
});
|
|
3939
|
+
if (!response.ok) {
|
|
3940
|
+
const errorText = await response.text();
|
|
3941
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3942
|
+
}
|
|
3943
|
+
return this.mapAssessmentResponse(await response.json());
|
|
3944
|
+
}
|
|
3945
|
+
async masfeatApproveAssessment(assessmentId, request) {
|
|
3946
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/assessments/${assessmentId}/approve`;
|
|
3947
|
+
const response = await fetch(url, {
|
|
3948
|
+
method: 'POST',
|
|
3949
|
+
headers: {
|
|
3950
|
+
'Content-Type': 'application/json',
|
|
3951
|
+
...this.getAuthHeaders(),
|
|
3952
|
+
},
|
|
3953
|
+
body: JSON.stringify({
|
|
3954
|
+
approved_by: request.approvedBy,
|
|
3955
|
+
comments: request.comments,
|
|
3956
|
+
}),
|
|
3957
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3958
|
+
});
|
|
3959
|
+
if (!response.ok) {
|
|
3960
|
+
const errorText = await response.text();
|
|
3961
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3962
|
+
}
|
|
3963
|
+
return this.mapAssessmentResponse(await response.json());
|
|
3964
|
+
}
|
|
3965
|
+
async masfeatRejectAssessment(assessmentId, request) {
|
|
3966
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/assessments/${assessmentId}/reject`;
|
|
3967
|
+
const response = await fetch(url, {
|
|
3968
|
+
method: 'POST',
|
|
3969
|
+
headers: {
|
|
3970
|
+
'Content-Type': 'application/json',
|
|
3971
|
+
...this.getAuthHeaders(),
|
|
3972
|
+
},
|
|
3973
|
+
body: JSON.stringify({
|
|
3974
|
+
rejected_by: request.rejectedBy,
|
|
3975
|
+
reason: request.reason,
|
|
3976
|
+
}),
|
|
3977
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3978
|
+
});
|
|
3979
|
+
if (!response.ok) {
|
|
3980
|
+
const errorText = await response.text();
|
|
3981
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3982
|
+
}
|
|
3983
|
+
return this.mapAssessmentResponse(await response.json());
|
|
3984
|
+
}
|
|
3985
|
+
// Kill Switch Methods
|
|
3986
|
+
async masfeatGetKillSwitch(systemId) {
|
|
3987
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}`;
|
|
3988
|
+
const response = await fetch(url, {
|
|
3989
|
+
method: 'GET',
|
|
3990
|
+
headers: {
|
|
3991
|
+
...this.getAuthHeaders(),
|
|
3992
|
+
},
|
|
3993
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
3994
|
+
});
|
|
3995
|
+
if (!response.ok) {
|
|
3996
|
+
const errorText = await response.text();
|
|
3997
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
3998
|
+
}
|
|
3999
|
+
return this.mapKillSwitchResponse(await response.json());
|
|
4000
|
+
}
|
|
4001
|
+
async masfeatConfigureKillSwitch(systemId, request) {
|
|
4002
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/configure`;
|
|
4003
|
+
const body = {};
|
|
4004
|
+
if (request.accuracyThreshold !== undefined)
|
|
4005
|
+
body.accuracy_threshold = request.accuracyThreshold;
|
|
4006
|
+
if (request.biasThreshold !== undefined)
|
|
4007
|
+
body.bias_threshold = request.biasThreshold;
|
|
4008
|
+
if (request.errorRateThreshold !== undefined)
|
|
4009
|
+
body.error_rate_threshold = request.errorRateThreshold;
|
|
4010
|
+
if (request.autoTriggerEnabled !== undefined)
|
|
4011
|
+
body.auto_trigger_enabled = request.autoTriggerEnabled;
|
|
4012
|
+
const response = await fetch(url, {
|
|
4013
|
+
method: 'POST',
|
|
4014
|
+
headers: {
|
|
4015
|
+
'Content-Type': 'application/json',
|
|
4016
|
+
...this.getAuthHeaders(),
|
|
4017
|
+
},
|
|
4018
|
+
body: JSON.stringify(body),
|
|
4019
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
4020
|
+
});
|
|
4021
|
+
if (!response.ok) {
|
|
4022
|
+
const errorText = await response.text();
|
|
4023
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
4024
|
+
}
|
|
4025
|
+
return this.mapKillSwitchResponse(await response.json());
|
|
4026
|
+
}
|
|
4027
|
+
async masfeatCheckKillSwitch(systemId, request) {
|
|
4028
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/check`;
|
|
4029
|
+
const response = await fetch(url, {
|
|
4030
|
+
method: 'POST',
|
|
4031
|
+
headers: {
|
|
4032
|
+
'Content-Type': 'application/json',
|
|
4033
|
+
...this.getAuthHeaders(),
|
|
4034
|
+
},
|
|
4035
|
+
body: JSON.stringify({
|
|
4036
|
+
accuracy: request.accuracy,
|
|
4037
|
+
bias_score: request.biasScore,
|
|
4038
|
+
error_rate: request.errorRate,
|
|
4039
|
+
}),
|
|
4040
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
4041
|
+
});
|
|
4042
|
+
if (!response.ok) {
|
|
4043
|
+
const errorText = await response.text();
|
|
4044
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
4045
|
+
}
|
|
4046
|
+
return this.mapKillSwitchResponse(await response.json());
|
|
4047
|
+
}
|
|
4048
|
+
async masfeatTriggerKillSwitch(systemId, request) {
|
|
4049
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/trigger`;
|
|
4050
|
+
const response = await fetch(url, {
|
|
4051
|
+
method: 'POST',
|
|
4052
|
+
headers: {
|
|
4053
|
+
'Content-Type': 'application/json',
|
|
4054
|
+
...this.getAuthHeaders(),
|
|
4055
|
+
},
|
|
4056
|
+
body: JSON.stringify({
|
|
4057
|
+
reason: request.reason,
|
|
4058
|
+
triggered_by: request.triggeredBy,
|
|
4059
|
+
}),
|
|
4060
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
4061
|
+
});
|
|
4062
|
+
if (!response.ok) {
|
|
4063
|
+
const errorText = await response.text();
|
|
4064
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
4065
|
+
}
|
|
4066
|
+
return this.mapKillSwitchResponse(await response.json());
|
|
4067
|
+
}
|
|
4068
|
+
async masfeatRestoreKillSwitch(systemId, request) {
|
|
4069
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/restore`;
|
|
4070
|
+
const response = await fetch(url, {
|
|
4071
|
+
method: 'POST',
|
|
4072
|
+
headers: {
|
|
4073
|
+
'Content-Type': 'application/json',
|
|
4074
|
+
...this.getAuthHeaders(),
|
|
4075
|
+
},
|
|
4076
|
+
body: JSON.stringify({
|
|
4077
|
+
reason: request.reason,
|
|
4078
|
+
restored_by: request.restoredBy,
|
|
4079
|
+
}),
|
|
4080
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
4081
|
+
});
|
|
4082
|
+
if (!response.ok) {
|
|
4083
|
+
const errorText = await response.text();
|
|
4084
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
4085
|
+
}
|
|
4086
|
+
return this.mapKillSwitchResponse(await response.json());
|
|
4087
|
+
}
|
|
4088
|
+
async masfeatEnableKillSwitch(systemId) {
|
|
4089
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/enable`;
|
|
4090
|
+
const response = await fetch(url, {
|
|
4091
|
+
method: 'POST',
|
|
4092
|
+
headers: {
|
|
4093
|
+
'Content-Type': 'application/json',
|
|
4094
|
+
...this.getAuthHeaders(),
|
|
4095
|
+
},
|
|
4096
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
4097
|
+
});
|
|
4098
|
+
if (!response.ok) {
|
|
4099
|
+
const errorText = await response.text();
|
|
4100
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
4101
|
+
}
|
|
4102
|
+
return this.mapKillSwitchResponse(await response.json());
|
|
4103
|
+
}
|
|
4104
|
+
async masfeatDisableKillSwitch(systemId, request) {
|
|
4105
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/disable`;
|
|
4106
|
+
const response = await fetch(url, {
|
|
4107
|
+
method: 'POST',
|
|
4108
|
+
headers: {
|
|
4109
|
+
'Content-Type': 'application/json',
|
|
4110
|
+
...this.getAuthHeaders(),
|
|
4111
|
+
},
|
|
4112
|
+
body: JSON.stringify({ reason: request?.reason }),
|
|
4113
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
4114
|
+
});
|
|
4115
|
+
if (!response.ok) {
|
|
4116
|
+
const errorText = await response.text();
|
|
4117
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
4118
|
+
}
|
|
4119
|
+
return this.mapKillSwitchResponse(await response.json());
|
|
4120
|
+
}
|
|
4121
|
+
async masfeatGetKillSwitchHistory(systemId, limit) {
|
|
4122
|
+
const params = new URLSearchParams();
|
|
4123
|
+
if (limit)
|
|
4124
|
+
params.append('limit', limit.toString());
|
|
4125
|
+
const queryString = params.toString();
|
|
4126
|
+
const url = `${this.config.endpoint}/api/v1/masfeat/killswitch/${systemId}/history${queryString ? `?${queryString}` : ''}`;
|
|
4127
|
+
const response = await fetch(url, {
|
|
4128
|
+
method: 'GET',
|
|
4129
|
+
headers: {
|
|
4130
|
+
...this.getAuthHeaders(),
|
|
4131
|
+
},
|
|
4132
|
+
signal: AbortSignal.timeout(this.config.timeout),
|
|
4133
|
+
});
|
|
4134
|
+
if (!response.ok) {
|
|
4135
|
+
const errorText = await response.text();
|
|
4136
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
4137
|
+
}
|
|
4138
|
+
let data = await response.json();
|
|
4139
|
+
// Handle nested response format {history: [...], count: N}
|
|
4140
|
+
if (data && typeof data === 'object' && 'history' in data) {
|
|
4141
|
+
data = data.history;
|
|
4142
|
+
}
|
|
4143
|
+
return (data || []).map((e) => ({
|
|
4144
|
+
id: e.id,
|
|
4145
|
+
killSwitchId: e.kill_switch_id,
|
|
4146
|
+
// Handle both API formats: event_type (SDK expected) vs action (API actual)
|
|
4147
|
+
eventType: e.event_type || e.action,
|
|
4148
|
+
// Build eventData from additional fields if not present
|
|
4149
|
+
eventData: e.event_data ||
|
|
4150
|
+
(e.previous_status || e.new_status || e.reason
|
|
4151
|
+
? { previousStatus: e.previous_status, newStatus: e.new_status, reason: e.reason }
|
|
4152
|
+
: undefined),
|
|
4153
|
+
// Handle both API formats: created_by vs performed_by
|
|
4154
|
+
createdBy: e.created_by || e.performed_by,
|
|
4155
|
+
// Handle both API formats: created_at vs performed_at
|
|
4156
|
+
createdAt: new Date(e.created_at || e.performed_at),
|
|
4157
|
+
}));
|
|
4158
|
+
}
|
|
4159
|
+
// Helper methods for MAS FEAT
|
|
4160
|
+
mapSystemResponse(data) {
|
|
4161
|
+
return {
|
|
4162
|
+
id: data.id,
|
|
4163
|
+
orgId: data.org_id,
|
|
4164
|
+
systemId: data.system_id,
|
|
4165
|
+
systemName: data.system_name,
|
|
4166
|
+
description: data.description,
|
|
4167
|
+
useCase: data.use_case,
|
|
4168
|
+
ownerTeam: data.owner_team,
|
|
4169
|
+
technicalOwner: data.technical_owner,
|
|
4170
|
+
businessOwner: data.business_owner || data.owner_email,
|
|
4171
|
+
customerImpact: data.customer_impact ?? data.risk_rating_impact,
|
|
4172
|
+
modelComplexity: data.model_complexity ?? data.risk_rating_complexity,
|
|
4173
|
+
humanReliance: data.human_reliance ?? data.risk_rating_reliance,
|
|
4174
|
+
materiality: data.materiality || data.materiality_classification,
|
|
4175
|
+
status: data.status,
|
|
4176
|
+
metadata: data.metadata,
|
|
4177
|
+
createdAt: new Date(data.created_at),
|
|
4178
|
+
updatedAt: new Date(data.updated_at),
|
|
4179
|
+
createdBy: data.created_by,
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
mapFindingResponse(data) {
|
|
4183
|
+
return {
|
|
4184
|
+
id: data.id,
|
|
4185
|
+
pillar: data.pillar,
|
|
4186
|
+
severity: data.severity,
|
|
4187
|
+
category: data.category,
|
|
4188
|
+
description: data.description,
|
|
4189
|
+
status: data.status,
|
|
4190
|
+
remediation: data.remediation,
|
|
4191
|
+
dueDate: data.due_date ? new Date(data.due_date) : undefined,
|
|
4192
|
+
};
|
|
4193
|
+
}
|
|
4194
|
+
mapAssessmentResponse(data) {
|
|
4195
|
+
return {
|
|
4196
|
+
id: data.id,
|
|
4197
|
+
orgId: data.org_id,
|
|
4198
|
+
systemId: data.system_id,
|
|
4199
|
+
assessmentType: data.assessment_type,
|
|
4200
|
+
status: data.status,
|
|
4201
|
+
assessmentDate: new Date(data.assessment_date),
|
|
4202
|
+
validUntil: data.valid_until ? new Date(data.valid_until) : undefined,
|
|
4203
|
+
fairnessScore: data.fairness_score,
|
|
4204
|
+
ethicsScore: data.ethics_score,
|
|
4205
|
+
accountabilityScore: data.accountability_score,
|
|
4206
|
+
transparencyScore: data.transparency_score,
|
|
4207
|
+
overallScore: data.overall_score,
|
|
4208
|
+
fairnessDetails: data.fairness_details,
|
|
4209
|
+
ethicsDetails: data.ethics_details,
|
|
4210
|
+
accountabilityDetails: data.accountability_details,
|
|
4211
|
+
transparencyDetails: data.transparency_details,
|
|
4212
|
+
findings: data.findings?.map((f) => this.mapFindingResponse(f)),
|
|
4213
|
+
recommendations: data.recommendations,
|
|
4214
|
+
assessors: data.assessors,
|
|
4215
|
+
approvedBy: data.approved_by,
|
|
4216
|
+
approvedAt: data.approved_at ? new Date(data.approved_at) : undefined,
|
|
4217
|
+
createdAt: new Date(data.created_at),
|
|
4218
|
+
updatedAt: new Date(data.updated_at),
|
|
4219
|
+
createdBy: data.created_by,
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
mapKillSwitchResponse(data) {
|
|
4223
|
+
// Handle nested response format (trigger/restore return {kill_switch: {...}, message: ...})
|
|
4224
|
+
if (data.kill_switch) {
|
|
4225
|
+
data = data.kill_switch;
|
|
4226
|
+
}
|
|
4227
|
+
return {
|
|
4228
|
+
id: data.id,
|
|
4229
|
+
orgId: data.org_id,
|
|
4230
|
+
systemId: data.system_id,
|
|
4231
|
+
status: data.status,
|
|
4232
|
+
accuracyThreshold: data.accuracy_threshold,
|
|
4233
|
+
biasThreshold: data.bias_threshold,
|
|
4234
|
+
errorRateThreshold: data.error_rate_threshold,
|
|
4235
|
+
autoTriggerEnabled: data.auto_trigger_enabled,
|
|
4236
|
+
triggeredAt: data.triggered_at ? new Date(data.triggered_at) : undefined,
|
|
4237
|
+
triggeredBy: data.triggered_by,
|
|
4238
|
+
triggeredReason: data.triggered_reason || data.trigger_reason,
|
|
4239
|
+
restoredAt: data.restored_at ? new Date(data.restored_at) : undefined,
|
|
4240
|
+
restoredBy: data.restored_by,
|
|
4241
|
+
createdAt: new Date(data.created_at),
|
|
4242
|
+
updatedAt: new Date(data.updated_at),
|
|
4243
|
+
};
|
|
4244
|
+
}
|
|
4245
|
+
// ============================================================================
|
|
4246
|
+
// Unified Execution Tracking Methods (Issue #1075)
|
|
4247
|
+
// ============================================================================
|
|
4248
|
+
/**
|
|
4249
|
+
* Get unified execution status for a MAP plan or WCP workflow.
|
|
4250
|
+
*
|
|
4251
|
+
* This method provides a consistent interface for tracking execution progress
|
|
4252
|
+
* regardless of whether the underlying execution is a MAP plan or WCP workflow.
|
|
4253
|
+
*
|
|
4254
|
+
* @param executionId - The execution ID (plan ID or workflow ID)
|
|
4255
|
+
* @returns Unified execution status
|
|
4256
|
+
*
|
|
4257
|
+
* @example
|
|
4258
|
+
* ```typescript
|
|
4259
|
+
* // Get status for any execution (MAP or WCP)
|
|
4260
|
+
* const status = await client.getExecutionStatus('exec_123');
|
|
4261
|
+
* console.log(`Type: ${status.execution_type}`);
|
|
4262
|
+
* console.log(`Status: ${status.status}`);
|
|
4263
|
+
* console.log(`Progress: ${status.progress_percent}%`);
|
|
4264
|
+
*
|
|
4265
|
+
* // Check steps
|
|
4266
|
+
* for (const step of status.steps) {
|
|
4267
|
+
* console.log(` Step ${step.step_index}: ${step.step_name} - ${step.status}`);
|
|
4268
|
+
* }
|
|
4269
|
+
* ```
|
|
4270
|
+
*/
|
|
4271
|
+
async getExecutionStatus(executionId) {
|
|
4272
|
+
if (!executionId) {
|
|
4273
|
+
throw new ConfigurationError('Execution ID is required');
|
|
4274
|
+
}
|
|
4275
|
+
if (this.config.debug) {
|
|
4276
|
+
debugLog('Getting execution status', { executionId });
|
|
4277
|
+
}
|
|
4278
|
+
return this.orchestratorRequest('GET', `/api/v1/unified/executions/${executionId}`);
|
|
4279
|
+
}
|
|
4280
|
+
/**
|
|
4281
|
+
* List unified executions with optional filters.
|
|
4282
|
+
*
|
|
4283
|
+
* Returns a paginated list of executions (both MAP plans and WCP workflows)
|
|
4284
|
+
* with optional filtering by type, status, tenant, or organization.
|
|
4285
|
+
* This method provides a unified view across all execution types.
|
|
4286
|
+
*
|
|
4287
|
+
* @param options - Filter and pagination options
|
|
4288
|
+
* @returns Paginated list of unified executions
|
|
4289
|
+
*
|
|
4290
|
+
* @example
|
|
4291
|
+
* ```typescript
|
|
4292
|
+
* // List all running executions
|
|
4293
|
+
* const result = await client.listUnifiedExecutions({
|
|
4294
|
+
* status: 'running',
|
|
4295
|
+
* limit: 20
|
|
4296
|
+
* });
|
|
4297
|
+
* console.log(`Found ${result.total} running executions`);
|
|
4298
|
+
*
|
|
4299
|
+
* // List only MAP plans
|
|
4300
|
+
* const mapPlans = await client.listUnifiedExecutions({
|
|
4301
|
+
* execution_type: 'map_plan',
|
|
4302
|
+
* limit: 50
|
|
4303
|
+
* });
|
|
4304
|
+
*
|
|
4305
|
+
* // List WCP workflows for a specific tenant
|
|
4306
|
+
* const workflows = await client.listUnifiedExecutions({
|
|
4307
|
+
* execution_type: 'wcp_workflow',
|
|
4308
|
+
* tenant_id: 'tenant_123'
|
|
4309
|
+
* });
|
|
4310
|
+
* ```
|
|
4311
|
+
*/
|
|
4312
|
+
async listUnifiedExecutions(options) {
|
|
4313
|
+
const params = new URLSearchParams();
|
|
4314
|
+
if (options?.execution_type) {
|
|
4315
|
+
params.set('execution_type', options.execution_type);
|
|
4316
|
+
}
|
|
4317
|
+
if (options?.status) {
|
|
4318
|
+
params.set('status', options.status);
|
|
4319
|
+
}
|
|
4320
|
+
if (options?.tenant_id) {
|
|
4321
|
+
params.set('tenant_id', options.tenant_id);
|
|
4322
|
+
}
|
|
4323
|
+
if (options?.org_id) {
|
|
4324
|
+
params.set('org_id', options.org_id);
|
|
4325
|
+
}
|
|
4326
|
+
if (options?.limit !== undefined) {
|
|
4327
|
+
params.set('limit', options.limit.toString());
|
|
4328
|
+
}
|
|
4329
|
+
if (options?.offset !== undefined) {
|
|
4330
|
+
params.set('offset', options.offset.toString());
|
|
4331
|
+
}
|
|
4332
|
+
const queryString = params.toString();
|
|
4333
|
+
const path = queryString
|
|
4334
|
+
? `/api/v1/unified/executions?${queryString}`
|
|
4335
|
+
: '/api/v1/unified/executions';
|
|
4336
|
+
if (this.config.debug) {
|
|
4337
|
+
debugLog('Listing unified executions', { options });
|
|
4338
|
+
}
|
|
4339
|
+
return this.orchestratorRequest('GET', path);
|
|
4340
|
+
}
|
|
4341
|
+
/**
|
|
4342
|
+
* Cancel a unified execution (MAP plan or WCP workflow).
|
|
4343
|
+
*
|
|
4344
|
+
* This method cancels an execution via the unified execution API,
|
|
4345
|
+
* automatically propagating to the correct subsystem (MAP or WCP).
|
|
4346
|
+
*
|
|
4347
|
+
* @param executionId - The execution ID (plan ID or workflow ID)
|
|
4348
|
+
* @param reason - Optional reason for cancellation
|
|
4349
|
+
*
|
|
4350
|
+
* @example
|
|
4351
|
+
* ```typescript
|
|
4352
|
+
* await client.cancelExecution('wf_abc123', 'User requested cancellation');
|
|
4353
|
+
* ```
|
|
4354
|
+
*/
|
|
4355
|
+
async cancelExecution(executionId, reason) {
|
|
4356
|
+
if (!executionId) {
|
|
4357
|
+
throw new ConfigurationError('Execution ID is required');
|
|
4358
|
+
}
|
|
4359
|
+
const body = reason ? { reason } : {};
|
|
4360
|
+
await this.orchestratorRequest('POST', `/api/v1/unified/executions/${executionId}/cancel`, body);
|
|
4361
|
+
}
|
|
4362
|
+
/**
|
|
4363
|
+
* Stream real-time execution status updates via Server-Sent Events (SSE).
|
|
4364
|
+
*
|
|
4365
|
+
* Connects to the SSE streaming endpoint and invokes the callback with each
|
|
4366
|
+
* ExecutionStatus update as it arrives. The stream automatically closes when
|
|
4367
|
+
* the execution reaches a terminal state (completed, failed, cancelled, aborted,
|
|
4368
|
+
* or expired).
|
|
4369
|
+
*
|
|
4370
|
+
* @param executionId - The execution ID (plan ID or workflow ID)
|
|
4371
|
+
* @param callback - Function called with each ExecutionStatus update
|
|
4372
|
+
* @param options - Optional configuration including an AbortSignal for cancellation
|
|
4373
|
+
*
|
|
4374
|
+
* @example
|
|
4375
|
+
* ```typescript
|
|
4376
|
+
* // Stream with a callback
|
|
4377
|
+
* await client.streamExecutionStatus('exec_123', (status) => {
|
|
4378
|
+
* console.log(`Progress: ${status.progress_percent}%`);
|
|
4379
|
+
* console.log(`Status: ${status.status}`);
|
|
4380
|
+
* for (const step of status.steps) {
|
|
4381
|
+
* console.log(` Step ${step.step_index}: ${step.step_name} - ${step.status}`);
|
|
4382
|
+
* }
|
|
4383
|
+
* });
|
|
4384
|
+
*
|
|
4385
|
+
* // Stream with abort support
|
|
4386
|
+
* const controller = new AbortController();
|
|
4387
|
+
* setTimeout(() => controller.abort(), 60000); // timeout after 1 minute
|
|
4388
|
+
* await client.streamExecutionStatus('exec_123', (status) => {
|
|
4389
|
+
* console.log(`${status.status}: ${status.progress_percent}%`);
|
|
4390
|
+
* }, { signal: controller.signal });
|
|
4391
|
+
* ```
|
|
4392
|
+
*/
|
|
4393
|
+
async streamExecutionStatus(executionId, callback, options) {
|
|
4394
|
+
if (!executionId) {
|
|
4395
|
+
throw new ConfigurationError('Execution ID is required');
|
|
4396
|
+
}
|
|
4397
|
+
const url = `${this.config.endpoint}/api/v1/executions/${executionId}/stream`;
|
|
4398
|
+
const headers = this.buildAuthHeaders();
|
|
4399
|
+
// Override Content-Type for SSE — Accept is what matters
|
|
4400
|
+
headers['Accept'] = 'text/event-stream';
|
|
4401
|
+
delete headers['Content-Type'];
|
|
4402
|
+
if (this.config.debug) {
|
|
4403
|
+
debugLog('Streaming execution status', { executionId, url });
|
|
4404
|
+
}
|
|
4405
|
+
const fetchOptions = {
|
|
4406
|
+
method: 'GET',
|
|
4407
|
+
headers,
|
|
4408
|
+
};
|
|
4409
|
+
if (options?.signal) {
|
|
4410
|
+
fetchOptions.signal = options.signal;
|
|
4411
|
+
}
|
|
4412
|
+
let response;
|
|
4413
|
+
try {
|
|
4414
|
+
response = await fetch(url, fetchOptions);
|
|
4415
|
+
}
|
|
4416
|
+
catch (error) {
|
|
4417
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
4418
|
+
return; // Clean exit on abort
|
|
4419
|
+
}
|
|
4420
|
+
throw error;
|
|
4421
|
+
}
|
|
4422
|
+
if (!response.ok) {
|
|
4423
|
+
const errorText = await response.text();
|
|
4424
|
+
if (response.status === 401 || response.status === 403) {
|
|
4425
|
+
throw new AuthenticationError(`Stream request failed: ${errorText}`);
|
|
4426
|
+
}
|
|
4427
|
+
if (response.status === 404) {
|
|
4428
|
+
throw new APIError(404, 'Not Found', errorText);
|
|
4429
|
+
}
|
|
4430
|
+
throw new APIError(response.status, response.statusText, errorText);
|
|
4431
|
+
}
|
|
4432
|
+
if (!response.body) {
|
|
4433
|
+
throw new APIError(0, 'No Body', 'SSE response has no body');
|
|
4434
|
+
}
|
|
4435
|
+
const reader = response.body.getReader();
|
|
4436
|
+
const decoder = new TextDecoder();
|
|
4437
|
+
let buffer = '';
|
|
4438
|
+
try {
|
|
4439
|
+
while (true) {
|
|
4440
|
+
const { done, value } = await reader.read();
|
|
4441
|
+
if (done) {
|
|
4442
|
+
break;
|
|
4443
|
+
}
|
|
4444
|
+
buffer += decoder.decode(value, { stream: true });
|
|
4445
|
+
// Process complete SSE events (separated by double newline)
|
|
4446
|
+
const events = buffer.split('\n\n');
|
|
4447
|
+
// Keep the last (potentially incomplete) chunk in the buffer
|
|
4448
|
+
buffer = events.pop() || '';
|
|
4449
|
+
for (const event of events) {
|
|
4450
|
+
const trimmed = event.trim();
|
|
4451
|
+
if (!trimmed) {
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
// Parse SSE data lines (handle both "data: " and "data:" formats per SSE spec)
|
|
4455
|
+
for (const line of trimmed.split('\n')) {
|
|
4456
|
+
let jsonStr;
|
|
4457
|
+
if (line.startsWith('data: ')) {
|
|
4458
|
+
jsonStr = line.slice(6);
|
|
4459
|
+
}
|
|
4460
|
+
else if (line.startsWith('data:')) {
|
|
4461
|
+
jsonStr = line.slice(5);
|
|
4462
|
+
}
|
|
4463
|
+
if (jsonStr !== undefined) {
|
|
4464
|
+
if (!jsonStr || jsonStr === '[DONE]') {
|
|
4465
|
+
continue;
|
|
4466
|
+
}
|
|
4467
|
+
try {
|
|
4468
|
+
const status = JSON.parse(jsonStr);
|
|
4469
|
+
callback(status);
|
|
4470
|
+
// Check for terminal status — stream is done
|
|
4471
|
+
if (status.status === 'completed' ||
|
|
4472
|
+
status.status === 'failed' ||
|
|
4473
|
+
status.status === 'cancelled' ||
|
|
4474
|
+
status.status === 'aborted' ||
|
|
4475
|
+
status.status === 'expired') {
|
|
4476
|
+
return;
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
catch (parseError) {
|
|
4480
|
+
if (this.config.debug) {
|
|
4481
|
+
debugLog('Failed to parse SSE data', { jsonStr, error: parseError });
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
catch (error) {
|
|
4490
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
4491
|
+
return; // Clean exit on abort
|
|
4492
|
+
}
|
|
4493
|
+
throw error;
|
|
4494
|
+
}
|
|
4495
|
+
finally {
|
|
4496
|
+
try {
|
|
4497
|
+
reader.releaseLock();
|
|
4498
|
+
}
|
|
4499
|
+
catch {
|
|
4500
|
+
// Reader may already be released
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
2789
4504
|
}
|
|
2790
4505
|
//# sourceMappingURL=client.js.map
|