Skip to content

Commit 57c9679

Browse files
authored
Merge pull request #171 from 0xbrainkid/feat/satp-agent-trust
feat(auth): add SATP agent trust verification provider
2 parents 7665c66 + 44bf363 commit 57c9679

3 files changed

Lines changed: 334 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# SATP Agent Trust Verification
2+
3+
Verify agent identity and behavioral trust scores using [AgentFolio/SATP](https://github.com/brainAI-bot/satp-solana-sdk) (Solana Agent Trust Protocol).
4+
5+
## Overview
6+
7+
The `SATPProvider` is an auth provider that checks an agent's on-chain trust score before allowing tool execution. It answers: **"Should I trust this agent for this task?"**
8+
9+
## Quick Start
10+
11+
```typescript
12+
import { MCPServer, SATPProvider } from "mcp-framework";
13+
14+
const server = new MCPServer({
15+
auth: {
16+
provider: new SATPProvider({
17+
minTrustScore: 50,
18+
onMissing: "allow", // Don't break unidentified agents
19+
}),
20+
},
21+
});
22+
```
23+
24+
No API keys needed — the AgentFolio API is public.
25+
26+
## Configuration
27+
28+
| Option | Type | Default | Description |
29+
|--------|------|---------|-------------|
30+
| `apiUrl` | `string` | `"https://api.agentfolio.bot"` | AgentFolio API base URL |
31+
| `minTrustScore` | `number` | `0` | Minimum trust score (0-100) to allow access |
32+
| `requireVerified` | `boolean` | `false` | Require on-chain verification |
33+
| `agentIdHeader` | `string` | `"x-agent-id"` | Header name for agent identity |
34+
| `onMissing` | `"allow" \| "reject"` | `"allow"` | Behavior when agent identity is missing |
35+
| `cacheTtlMs` | `number` | `300000` | Cache TTL in ms (default 5 min) |
36+
37+
## How It Works
38+
39+
1. Agent sends request with `x-agent-id` header (or `Authorization: Agent <id>`)
40+
2. Provider queries AgentFolio for the agent's trust data
41+
3. If trust score meets threshold → request proceeds with trust data attached
42+
4. If below threshold → request rejected with `403` and `X-Trust-Required` header
43+
44+
## Modes
45+
46+
### Annotation mode (default)
47+
```typescript
48+
new SATPProvider({ onMissing: "allow", minTrustScore: 0 })
49+
```
50+
All requests pass through. Trust data is attached to `AuthResult.data.agentTrust` for your tool handlers to use (or ignore).
51+
52+
### Enforcement mode
53+
```typescript
54+
new SATPProvider({
55+
minTrustScore: 50,
56+
requireVerified: true,
57+
onMissing: "reject"
58+
})
59+
```
60+
Only verified agents with trust score ≥ 50 can access tools. Unidentified requests are rejected.
61+
62+
### Graduated trust
63+
```typescript
64+
// In your tool handler, use the trust data for risk-based decisions
65+
const trust = request.context?.agentTrust;
66+
67+
if (trust?.trustScore > 80) {
68+
// High trust: allow sensitive operations
69+
} else if (trust?.trustScore > 30) {
70+
// Medium trust: allow read-only operations
71+
} else {
72+
// Low/no trust: sandbox mode
73+
}
74+
```
75+
76+
## Testing
77+
78+
```bash
79+
# Test with a verified agent
80+
curl -H "x-agent-id: brainGrowth" http://localhost:3000/mcp
81+
82+
# Test without identity (annotation mode passes through)
83+
curl http://localhost:3000/mcp
84+
```
85+
86+
## Trust Data Shape
87+
88+
```typescript
89+
interface AgentTrustResult {
90+
agentId: string; // Agent identifier
91+
trustScore: number; // 0-100
92+
verified: boolean; // On-chain verification status
93+
name?: string; // Display name
94+
capabilities?: string[]; // Capability tags
95+
lastVerified?: string; // ISO timestamp
96+
}
97+
```
98+
99+
## Composing with Other Providers
100+
101+
SATPProvider works alongside JWT, OAuth, or API key providers. Use it as a secondary check after authentication:
102+
103+
```typescript
104+
// Your custom composed provider
105+
class ComposedProvider implements AuthProvider {
106+
private jwt = new JWTProvider(jwtConfig);
107+
private satp = new SATPProvider({ minTrustScore: 30 });
108+
109+
async authenticate(req: IncomingMessage) {
110+
const jwtResult = await this.jwt.authenticate(req);
111+
if (!jwtResult) return false;
112+
113+
const satpResult = await this.satp.authenticate(req);
114+
return satpResult; // Trust data in result.data.agentTrust
115+
}
116+
}
117+
```

src/auth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from "./types.js";
22
export * from "./providers/jwt.js";
33
export * from "./providers/apikey.js";
44
export * from "./providers/oauth.js";
5+
export * from "./providers/satp.js";
56

67
export type { AuthProvider, AuthConfig, AuthResult } from "./types.js";
78
export type { JWTConfig } from "./providers/jwt.js";

src/auth/providers/satp.ts

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { IncomingMessage } from "node:http";
2+
import { AuthProvider, AuthResult, DEFAULT_AUTH_ERROR } from "../types.js";
3+
4+
/**
5+
* Trust verification result from SATP/AgentFolio
6+
*/
7+
export interface AgentTrustResult {
8+
/** Agent identifier */
9+
agentId: string;
10+
/** Trust score (0-100) */
11+
trustScore: number;
12+
/** Whether the agent is verified on-chain */
13+
verified: boolean;
14+
/** Agent display name */
15+
name?: string;
16+
/** Capabilities tags */
17+
capabilities?: string[];
18+
/** Last verification timestamp */
19+
lastVerified?: string;
20+
}
21+
22+
/**
23+
* Configuration for SATP agent trust verification
24+
*/
25+
export interface SATPConfig {
26+
/**
27+
* AgentFolio API base URL
28+
* @default "https://api.agentfolio.bot"
29+
*/
30+
apiUrl?: string;
31+
32+
/**
33+
* Minimum trust score required (0-100)
34+
* Set to 0 to allow all agents but still annotate requests with trust data
35+
* @default 0
36+
*/
37+
minTrustScore?: number;
38+
39+
/**
40+
* Require on-chain verification
41+
* @default false
42+
*/
43+
requireVerified?: boolean;
44+
45+
/**
46+
* Header name for agent identity
47+
* @default "x-agent-id"
48+
*/
49+
agentIdHeader?: string;
50+
51+
/**
52+
* Behavior when agent identity is missing from request
53+
* - "reject": Return 401
54+
* - "allow": Continue without trust data
55+
* @default "allow"
56+
*/
57+
onMissing?: "reject" | "allow";
58+
59+
/**
60+
* Cache TTL in milliseconds for trust score lookups
61+
* @default 300000 (5 minutes)
62+
*/
63+
cacheTtlMs?: number;
64+
}
65+
66+
/**
67+
* SATP Agent Trust Provider
68+
*
69+
* Verifies agent identity and trust scores via AgentFolio/SATP.
70+
* Can be used standalone or composed with other auth providers.
71+
*
72+
* @example
73+
* ```typescript
74+
* import { MCPServer, SATPProvider } from "mcp-framework";
75+
*
76+
* const server = new MCPServer({
77+
* auth: {
78+
* provider: new SATPProvider({
79+
* minTrustScore: 50,
80+
* requireVerified: true,
81+
* }),
82+
* },
83+
* });
84+
* ```
85+
*/
86+
export class SATPProvider implements AuthProvider {
87+
private config: Required<SATPConfig>;
88+
private cache: Map<string, { result: AgentTrustResult; expiry: number }> =
89+
new Map();
90+
91+
constructor(config: SATPConfig = {}) {
92+
this.config = {
93+
apiUrl: config.apiUrl ?? "https://api.agentfolio.bot",
94+
minTrustScore: config.minTrustScore ?? 0,
95+
requireVerified: config.requireVerified ?? false,
96+
agentIdHeader: config.agentIdHeader ?? "x-agent-id",
97+
onMissing: config.onMissing ?? "allow",
98+
cacheTtlMs: config.cacheTtlMs ?? 300_000,
99+
};
100+
}
101+
102+
async authenticate(req: IncomingMessage): Promise<boolean | AuthResult> {
103+
const agentId = this.extractAgentId(req);
104+
105+
if (!agentId) {
106+
return this.config.onMissing === "allow" ? { data: { agentTrust: null } } : false;
107+
}
108+
109+
const trust = await this.queryTrust(agentId);
110+
111+
if (!trust) {
112+
return this.config.onMissing === "allow" ? { data: { agentTrust: null } } : false;
113+
}
114+
115+
// Check minimum trust score
116+
if (trust.trustScore < this.config.minTrustScore) {
117+
return false;
118+
}
119+
120+
// Check verification requirement
121+
if (this.config.requireVerified && !trust.verified) {
122+
return false;
123+
}
124+
125+
return {
126+
data: {
127+
agentTrust: trust,
128+
},
129+
};
130+
}
131+
132+
getAuthError(): { status: number; message: string; headers?: Record<string, string> } {
133+
return {
134+
status: 403,
135+
message: "Agent trust verification failed",
136+
headers: {
137+
"X-Trust-Required": `min-score=${this.config.minTrustScore}`,
138+
},
139+
};
140+
}
141+
142+
/**
143+
* Extract agent ID from request headers or MCP metadata
144+
*/
145+
private extractAgentId(req: IncomingMessage): string | null {
146+
// Check custom header first
147+
const headerValue = req.headers[this.config.agentIdHeader];
148+
if (headerValue) {
149+
return Array.isArray(headerValue) ? headerValue[0] : headerValue;
150+
}
151+
152+
// Check Authorization header for agent token
153+
const auth = req.headers.authorization;
154+
if (auth?.startsWith("Agent ")) {
155+
return auth.slice(6).trim();
156+
}
157+
158+
return null;
159+
}
160+
161+
/**
162+
* Query AgentFolio API for agent trust data with caching
163+
*/
164+
private async queryTrust(agentId: string): Promise<AgentTrustResult | null> {
165+
// Check cache
166+
const cached = this.cache.get(agentId);
167+
if (cached && cached.expiry > Date.now()) {
168+
return cached.result;
169+
}
170+
171+
try {
172+
const response = await fetch(
173+
`${this.config.apiUrl}/v1/agents/${encodeURIComponent(agentId)}/trust`,
174+
{
175+
method: "GET",
176+
headers: {
177+
Accept: "application/json",
178+
"User-Agent": "mcp-framework-satp/1.0",
179+
},
180+
signal: AbortSignal.timeout(5000),
181+
}
182+
);
183+
184+
if (!response.ok) {
185+
return null;
186+
}
187+
188+
const data = (await response.json()) as AgentTrustResult;
189+
const result: AgentTrustResult = {
190+
agentId: data.agentId ?? agentId,
191+
trustScore: data.trustScore ?? 0,
192+
verified: data.verified ?? false,
193+
name: data.name,
194+
capabilities: data.capabilities,
195+
lastVerified: data.lastVerified,
196+
};
197+
198+
// Cache result
199+
this.cache.set(agentId, {
200+
result,
201+
expiry: Date.now() + this.config.cacheTtlMs,
202+
});
203+
204+
return result;
205+
} catch {
206+
return null;
207+
}
208+
}
209+
210+
/**
211+
* Clear the trust score cache
212+
*/
213+
clearCache(): void {
214+
this.cache.clear();
215+
}
216+
}

0 commit comments

Comments
 (0)