@kya-os/mcp-i 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Authorization Handshake Module
3
+ *
4
+ * Orchestrates the authorization flow for MCP-I bouncer:
5
+ * 1. Check agent reputation (optional)
6
+ * 2. Verify delegation exists
7
+ * 3. Return needs_authorization error if missing
8
+ *
9
+ * This module implements the "gatekeeper" logic that determines whether
10
+ * an agent should be allowed to execute a tool or needs human authorization.
11
+ *
12
+ * Flow:
13
+ * - If delegation exists + valid → allow (fast path)
14
+ * - If delegation missing → return needs_authorization with hints
15
+ * - If reputation too low (optional) → require authorization
16
+ *
17
+ * Related: PHASE_1_XMCP_I_SERVER.md Epic 2 (Runtime Interceptor)
18
+ */
19
+ import { NeedsAuthorizationError } from '@kya-os/contracts/runtime';
20
+ import { DelegationVerifier } from './delegation-verifier';
21
+ import { DelegationRecord } from '@kya-os/contracts/delegation';
22
+ /**
23
+ * Agent reputation data from KTA
24
+ */
25
+ export interface AgentReputation {
26
+ /** Agent DID */
27
+ agentDid: string;
28
+ /** Reputation score (0-100) */
29
+ score: number;
30
+ /** Total interactions recorded */
31
+ totalInteractions: number;
32
+ /** Success rate (0-1) */
33
+ successRate: number;
34
+ /** Risk level assessment */
35
+ riskLevel: 'low' | 'medium' | 'high' | 'unknown';
36
+ /** Last updated timestamp */
37
+ updatedAt: number;
38
+ }
39
+ /**
40
+ * Configuration for auth handshake
41
+ */
42
+ export interface AuthHandshakeConfig {
43
+ /** Delegation verifier instance */
44
+ delegationVerifier: DelegationVerifier;
45
+ /** KTA API configuration (optional, for reputation checks) */
46
+ kta?: {
47
+ apiUrl: string;
48
+ apiKey?: string;
49
+ };
50
+ /** Bouncer configuration */
51
+ bouncer: {
52
+ /** Authorization URL base (e.g., "https://agentshield.example.com/consent") */
53
+ authorizationUrl: string;
54
+ /** Resume token TTL in milliseconds (default: 10 minutes) */
55
+ resumeTokenTtl?: number;
56
+ /** Whether to require authorization for unknown/untrusted agents */
57
+ requireAuthForUnknown?: boolean;
58
+ /** Minimum reputation score to bypass authorization (0-100) */
59
+ minReputationScore?: number;
60
+ };
61
+ /** Enable debug logging */
62
+ debug?: boolean;
63
+ }
64
+ /**
65
+ * Result of auth handshake verification
66
+ */
67
+ export interface VerifyOrHintsResult {
68
+ /** Whether authorization is granted */
69
+ authorized: boolean;
70
+ /** Delegation record (if authorized) */
71
+ delegation?: DelegationRecord;
72
+ /** needs_authorization error (if not authorized) */
73
+ authError?: NeedsAuthorizationError;
74
+ /** Agent reputation data (if available) */
75
+ reputation?: AgentReputation;
76
+ /** Reason for decision */
77
+ reason?: string;
78
+ }
79
+ /**
80
+ * Resume token store interface
81
+ *
82
+ * Stores short-lived tokens for resuming after authorization
83
+ */
84
+ export interface ResumeTokenStore {
85
+ /**
86
+ * Create resume token
87
+ *
88
+ * @param agentDid - Agent DID
89
+ * @param scopes - Required scopes
90
+ * @param metadata - Optional metadata (user agent, IP, etc.)
91
+ * @returns Resume token string
92
+ */
93
+ create(agentDid: string, scopes: string[], metadata?: Record<string, any>): Promise<string>;
94
+ /**
95
+ * Get resume token data
96
+ *
97
+ * @param token - Resume token
98
+ * @returns Token data or null if expired/invalid
99
+ */
100
+ get(token: string): Promise<{
101
+ agentDid: string;
102
+ scopes: string[];
103
+ createdAt: number;
104
+ expiresAt: number;
105
+ metadata?: Record<string, any>;
106
+ } | null>;
107
+ /**
108
+ * Mark token as fulfilled (one-time use)
109
+ *
110
+ * @param token - Resume token
111
+ */
112
+ fulfill(token: string): Promise<void>;
113
+ }
114
+ /**
115
+ * Simple in-memory resume token store (for testing/development)
116
+ */
117
+ export declare class MemoryResumeTokenStore implements ResumeTokenStore {
118
+ private tokens;
119
+ private ttl;
120
+ constructor(ttlMs?: number);
121
+ create(agentDid: string, scopes: string[], metadata?: Record<string, any>): Promise<string>;
122
+ get(token: string): Promise<any | null>;
123
+ fulfill(token: string): Promise<void>;
124
+ clear(): void;
125
+ }
126
+ /**
127
+ * Main auth handshake function
128
+ *
129
+ * Verifies agent authorization or returns authorization hints
130
+ *
131
+ * @param agentDid - Agent DID requesting access
132
+ * @param scopes - Required permission scopes
133
+ * @param config - Auth handshake configuration
134
+ * @param resumeToken - Optional resume token from previous authorization
135
+ * @returns Verification result with delegation or auth error
136
+ */
137
+ export declare function verifyOrHints(agentDid: string, scopes: string[], config: AuthHandshakeConfig, resumeToken?: string): Promise<VerifyOrHintsResult>;
138
+ /**
139
+ * Helper: Check if scopes are sensitive and require authorization
140
+ *
141
+ * @param scopes - Scopes to check
142
+ * @returns true if scopes are sensitive
143
+ */
144
+ export declare function hasSensitiveScopes(scopes: string[]): boolean;
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ /**
3
+ * Authorization Handshake Module
4
+ *
5
+ * Orchestrates the authorization flow for MCP-I bouncer:
6
+ * 1. Check agent reputation (optional)
7
+ * 2. Verify delegation exists
8
+ * 3. Return needs_authorization error if missing
9
+ *
10
+ * This module implements the "gatekeeper" logic that determines whether
11
+ * an agent should be allowed to execute a tool or needs human authorization.
12
+ *
13
+ * Flow:
14
+ * - If delegation exists + valid → allow (fast path)
15
+ * - If delegation missing → return needs_authorization with hints
16
+ * - If reputation too low (optional) → require authorization
17
+ *
18
+ * Related: PHASE_1_XMCP_I_SERVER.md Epic 2 (Runtime Interceptor)
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.MemoryResumeTokenStore = void 0;
22
+ exports.verifyOrHints = verifyOrHints;
23
+ exports.hasSensitiveScopes = hasSensitiveScopes;
24
+ const runtime_1 = require("@kya-os/contracts/runtime");
25
+ /**
26
+ * Simple in-memory resume token store (for testing/development)
27
+ */
28
+ class MemoryResumeTokenStore {
29
+ tokens = new Map();
30
+ ttl;
31
+ constructor(ttlMs = 600_000) {
32
+ this.ttl = ttlMs;
33
+ }
34
+ async create(agentDid, scopes, metadata) {
35
+ const token = `rt_${Date.now()}_${Math.random().toString(36).substr(2, 16)}`;
36
+ const now = Date.now();
37
+ this.tokens.set(token, {
38
+ agentDid,
39
+ scopes,
40
+ createdAt: now,
41
+ expiresAt: now + this.ttl,
42
+ metadata,
43
+ fulfilled: false,
44
+ });
45
+ return token;
46
+ }
47
+ async get(token) {
48
+ const data = this.tokens.get(token);
49
+ if (!data)
50
+ return null;
51
+ // Check expiration
52
+ if (Date.now() > data.expiresAt) {
53
+ this.tokens.delete(token);
54
+ return null;
55
+ }
56
+ // Check if already fulfilled
57
+ if (data.fulfilled) {
58
+ return null;
59
+ }
60
+ return data;
61
+ }
62
+ async fulfill(token) {
63
+ const data = this.tokens.get(token);
64
+ if (data) {
65
+ data.fulfilled = true;
66
+ }
67
+ }
68
+ clear() {
69
+ this.tokens.clear();
70
+ }
71
+ }
72
+ exports.MemoryResumeTokenStore = MemoryResumeTokenStore;
73
+ /**
74
+ * Main auth handshake function
75
+ *
76
+ * Verifies agent authorization or returns authorization hints
77
+ *
78
+ * @param agentDid - Agent DID requesting access
79
+ * @param scopes - Required permission scopes
80
+ * @param config - Auth handshake configuration
81
+ * @param resumeToken - Optional resume token from previous authorization
82
+ * @returns Verification result with delegation or auth error
83
+ */
84
+ async function verifyOrHints(agentDid, scopes, config, resumeToken) {
85
+ const startTime = Date.now();
86
+ if (config.debug) {
87
+ console.log(`[AuthHandshake] Verifying ${agentDid} for scopes: ${scopes.join(', ')}`);
88
+ }
89
+ // Step 1: Check reputation (optional, if KTA configured)
90
+ let reputation;
91
+ if (config.kta && config.bouncer.minReputationScore !== undefined) {
92
+ try {
93
+ reputation = await fetchAgentReputation(agentDid, config.kta);
94
+ if (config.debug) {
95
+ console.log(`[AuthHandshake] Reputation score: ${reputation.score}`);
96
+ }
97
+ // If reputation is too low, require authorization
98
+ if (reputation.score < config.bouncer.minReputationScore) {
99
+ if (config.debug) {
100
+ console.log(`[AuthHandshake] Reputation ${reputation.score} < ${config.bouncer.minReputationScore}, requiring authorization`);
101
+ }
102
+ const authError = await buildNeedsAuthorizationError(agentDid, scopes, config, 'Agent reputation score below threshold');
103
+ return {
104
+ authorized: false,
105
+ authError,
106
+ reputation,
107
+ reason: 'Low reputation score',
108
+ };
109
+ }
110
+ }
111
+ catch (error) {
112
+ // Don't fail hard on reputation check failure
113
+ console.warn('[AuthHandshake] Failed to check reputation:', error);
114
+ }
115
+ }
116
+ // Step 2: Check for existing delegation
117
+ let delegationResult;
118
+ try {
119
+ delegationResult = await config.delegationVerifier.verify(agentDid, scopes);
120
+ }
121
+ catch (error) {
122
+ console.error('[AuthHandshake] Delegation verification failed:', error);
123
+ const errorMessage = `Delegation verification error: ${error instanceof Error ? error.message : 'Unknown error'}`;
124
+ const authError = await buildNeedsAuthorizationError(agentDid, scopes, config, errorMessage);
125
+ return {
126
+ authorized: false,
127
+ authError,
128
+ reason: errorMessage,
129
+ };
130
+ }
131
+ // Step 3: If delegation exists and valid, authorize immediately
132
+ if (delegationResult.valid && delegationResult.delegation) {
133
+ if (config.debug) {
134
+ console.log(`[AuthHandshake] Delegation valid, authorized (${Date.now() - startTime}ms)`);
135
+ }
136
+ return {
137
+ authorized: true,
138
+ delegation: delegationResult.delegation,
139
+ reputation,
140
+ reason: 'Valid delegation found',
141
+ };
142
+ }
143
+ // Step 4: No delegation found - return needs_authorization error
144
+ if (config.debug) {
145
+ console.log(`[AuthHandshake] No delegation found, returning needs_authorization (${Date.now() - startTime}ms)`);
146
+ }
147
+ const authError = await buildNeedsAuthorizationError(agentDid, scopes, config, delegationResult.reason || 'No valid delegation found');
148
+ return {
149
+ authorized: false,
150
+ authError,
151
+ reputation,
152
+ reason: delegationResult.reason || 'No delegation',
153
+ };
154
+ }
155
+ /**
156
+ * Fetch agent reputation from KTA
157
+ *
158
+ * @param agentDid - Agent DID
159
+ * @param ktaConfig - KTA API configuration
160
+ * @returns Agent reputation data
161
+ */
162
+ async function fetchAgentReputation(agentDid, ktaConfig) {
163
+ const apiUrl = ktaConfig.apiUrl.replace(/\/$/, '');
164
+ const headers = {
165
+ 'Content-Type': 'application/json',
166
+ };
167
+ if (ktaConfig.apiKey) {
168
+ headers['X-API-Key'] = ktaConfig.apiKey;
169
+ }
170
+ const response = await fetch(`${apiUrl}/api/v1/reputation/${encodeURIComponent(agentDid)}`, {
171
+ method: 'GET',
172
+ headers,
173
+ });
174
+ if (!response.ok) {
175
+ if (response.status === 404) {
176
+ // Agent not registered, return default "unknown" reputation
177
+ return {
178
+ agentDid,
179
+ score: 50, // Neutral score
180
+ totalInteractions: 0,
181
+ successRate: 0,
182
+ riskLevel: 'unknown',
183
+ updatedAt: Date.now(),
184
+ };
185
+ }
186
+ throw new Error(`KTA API error: ${response.status} ${response.statusText}`);
187
+ }
188
+ const data = await response.json();
189
+ return {
190
+ agentDid: data.agentDid || agentDid,
191
+ score: data.score || 50,
192
+ totalInteractions: data.totalInteractions || 0,
193
+ successRate: data.successRate || 0,
194
+ riskLevel: data.riskLevel || 'unknown',
195
+ updatedAt: data.updatedAt || Date.now(),
196
+ };
197
+ }
198
+ /**
199
+ * Build needs_authorization error with hints
200
+ *
201
+ * @param agentDid - Agent DID
202
+ * @param scopes - Required scopes
203
+ * @param config - Auth handshake configuration
204
+ * @param message - Human-readable error message
205
+ * @returns NeedsAuthorizationError
206
+ */
207
+ async function buildNeedsAuthorizationError(agentDid, scopes, config, message) {
208
+ // Generate resume token (simple implementation - use proper store in production)
209
+ const resumeTokenStore = new MemoryResumeTokenStore(config.bouncer.resumeTokenTtl || 600_000);
210
+ const resumeToken = await resumeTokenStore.create(agentDid, scopes, {
211
+ requestedAt: Date.now(),
212
+ });
213
+ const expiresAt = Date.now() + (config.bouncer.resumeTokenTtl || 600_000);
214
+ // Build authorization URL
215
+ const authUrl = new URL(config.bouncer.authorizationUrl);
216
+ authUrl.searchParams.set('agent_did', agentDid);
217
+ authUrl.searchParams.set('scopes', scopes.join(','));
218
+ authUrl.searchParams.set('resume_token', resumeToken);
219
+ // Generate short authorization code (for display)
220
+ const authCode = resumeToken.substring(0, 8).toUpperCase();
221
+ // Build display hints
222
+ const display = {
223
+ title: 'Authorization Required',
224
+ hint: ['link', 'qr'],
225
+ authorizationCode: authCode,
226
+ qrUrl: `https://chart.googleapis.com/chart?cht=qr&chs=300x300&chl=${encodeURIComponent(authUrl.toString())}`,
227
+ };
228
+ return (0, runtime_1.createNeedsAuthorizationError)({
229
+ message,
230
+ authorizationUrl: authUrl.toString(),
231
+ resumeToken,
232
+ expiresAt,
233
+ scopes,
234
+ display,
235
+ });
236
+ }
237
+ /**
238
+ * Helper: Check if scopes are sensitive and require authorization
239
+ *
240
+ * @param scopes - Scopes to check
241
+ * @returns true if scopes are sensitive
242
+ */
243
+ function hasSensitiveScopes(scopes) {
244
+ const sensitivePatterns = [
245
+ 'write',
246
+ 'delete',
247
+ 'admin',
248
+ 'payment',
249
+ 'transfer',
250
+ 'execute',
251
+ 'modify',
252
+ ];
253
+ return scopes.some((scope) => sensitivePatterns.some((pattern) => scope.toLowerCase().includes(pattern)));
254
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * AgentShield API Delegation Verifier
3
+ *
4
+ * Queries delegations from AgentShield managed service API.
5
+ * Includes local caching to minimize network calls and maximize performance.
6
+ *
7
+ * Performance:
8
+ * - Fast path (cached): < 5ms
9
+ * - Slow path (API call): < 100ms
10
+ * - Cache TTL: 1 minute (configurable)
11
+ *
12
+ * API Endpoints:
13
+ * - POST /api/v1/delegations/verify - Verify delegation by agent DID + scopes
14
+ * - GET /api/v1/delegations/:id - Get delegation by ID
15
+ * - POST /api/v1/delegations - Create new delegation (admin only)
16
+ * - POST /api/v1/delegations/:id/revoke - Revoke delegation (admin only)
17
+ *
18
+ * Authentication: Bearer token via X-API-Key header
19
+ *
20
+ * Related: PHASE_1_XMCP_I_SERVER.md Ticket 1.3
21
+ * Related: AGENTSHIELD_DASHBOARD_PLAN.md Epic 2 (Public API)
22
+ */
23
+ import { DelegationRecord } from '@kya-os/contracts/delegation';
24
+ import { DelegationVerifier, DelegationVerifierConfig, VerifyDelegationResult, VerifyDelegationOptions } from './delegation-verifier';
25
+ /**
26
+ * AgentShield API Delegation Verifier
27
+ *
28
+ * Managed mode: Queries delegations from AgentShield dashboard API
29
+ */
30
+ export declare class AgentShieldAPIDelegationVerifier implements DelegationVerifier {
31
+ private apiUrl;
32
+ private apiKey;
33
+ private cache;
34
+ private cacheTtl;
35
+ private debug;
36
+ constructor(config: DelegationVerifierConfig);
37
+ /**
38
+ * Verify agent delegation via API
39
+ */
40
+ verify(agentDid: string, scopes: string[], options?: VerifyDelegationOptions): Promise<VerifyDelegationResult>;
41
+ /**
42
+ * Get delegation by ID via API
43
+ */
44
+ get(delegationId: string): Promise<DelegationRecord | null>;
45
+ /**
46
+ * Store delegation (admin operation via API)
47
+ *
48
+ * Note: This is typically done via AgentShield dashboard UI,
49
+ * not by the bouncer directly. Included for completeness.
50
+ */
51
+ put(delegation: DelegationRecord): Promise<void>;
52
+ /**
53
+ * Revoke delegation via API
54
+ */
55
+ revoke(delegationId: string, reason?: string): Promise<void>;
56
+ /**
57
+ * Close connections (cleanup)
58
+ */
59
+ close(): Promise<void>;
60
+ /**
61
+ * Build deterministic scopes key for caching
62
+ */
63
+ private buildScopesKey;
64
+ }