Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions docs/docs-package/satp-agent-trust.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# SATP Agent Trust Verification

Verify agent identity and behavioral trust scores using [AgentFolio/SATP](https://github.com/brainAI-bot/satp-solana-sdk) (Solana Agent Trust Protocol).

## Overview

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?"**

## Quick Start

```typescript
import { MCPServer, SATPProvider } from "mcp-framework";

const server = new MCPServer({
auth: {
provider: new SATPProvider({
minTrustScore: 50,
onMissing: "allow", // Don't break unidentified agents
}),
},
});
```

No API keys needed — the AgentFolio API is public.

## Configuration

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiUrl` | `string` | `"https://api.agentfolio.bot"` | AgentFolio API base URL |
| `minTrustScore` | `number` | `0` | Minimum trust score (0-100) to allow access |
| `requireVerified` | `boolean` | `false` | Require on-chain verification |
| `agentIdHeader` | `string` | `"x-agent-id"` | Header name for agent identity |
| `onMissing` | `"allow" \| "reject"` | `"allow"` | Behavior when agent identity is missing |
| `cacheTtlMs` | `number` | `300000` | Cache TTL in ms (default 5 min) |

## How It Works

1. Agent sends request with `x-agent-id` header (or `Authorization: Agent <id>`)
2. Provider queries AgentFolio for the agent's trust data
3. If trust score meets threshold → request proceeds with trust data attached
4. If below threshold → request rejected with `403` and `X-Trust-Required` header

## Modes

### Annotation mode (default)
```typescript
new SATPProvider({ onMissing: "allow", minTrustScore: 0 })
```
All requests pass through. Trust data is attached to `AuthResult.data.agentTrust` for your tool handlers to use (or ignore).

### Enforcement mode
```typescript
new SATPProvider({
minTrustScore: 50,
requireVerified: true,
onMissing: "reject"
})
```
Only verified agents with trust score ≥ 50 can access tools. Unidentified requests are rejected.

### Graduated trust
```typescript
// In your tool handler, use the trust data for risk-based decisions
const trust = request.context?.agentTrust;

if (trust?.trustScore > 80) {
// High trust: allow sensitive operations
} else if (trust?.trustScore > 30) {
// Medium trust: allow read-only operations
} else {
// Low/no trust: sandbox mode
}
```

## Testing

```bash
# Test with a verified agent
curl -H "x-agent-id: brainGrowth" http://localhost:3000/mcp

# Test without identity (annotation mode passes through)
curl http://localhost:3000/mcp
```

## Trust Data Shape

```typescript
interface AgentTrustResult {
agentId: string; // Agent identifier
trustScore: number; // 0-100
verified: boolean; // On-chain verification status
name?: string; // Display name
capabilities?: string[]; // Capability tags
lastVerified?: string; // ISO timestamp
}
```

## Composing with Other Providers

SATPProvider works alongside JWT, OAuth, or API key providers. Use it as a secondary check after authentication:

```typescript
// Your custom composed provider
class ComposedProvider implements AuthProvider {
private jwt = new JWTProvider(jwtConfig);
private satp = new SATPProvider({ minTrustScore: 30 });

async authenticate(req: IncomingMessage) {
const jwtResult = await this.jwt.authenticate(req);
if (!jwtResult) return false;

const satpResult = await this.satp.authenticate(req);
return satpResult; // Trust data in result.data.agentTrust
}
}
```
1 change: 1 addition & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./types.js";
export * from "./providers/jwt.js";
export * from "./providers/apikey.js";
export * from "./providers/oauth.js";
export * from "./providers/satp.js";

export type { AuthProvider, AuthConfig, AuthResult } from "./types.js";
export type { JWTConfig } from "./providers/jwt.js";
Expand Down
216 changes: 216 additions & 0 deletions src/auth/providers/satp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { IncomingMessage } from "node:http";
import { AuthProvider, AuthResult, DEFAULT_AUTH_ERROR } from "../types.js";

/**
* Trust verification result from SATP/AgentFolio
*/
export interface AgentTrustResult {
/** Agent identifier */
agentId: string;
/** Trust score (0-100) */
trustScore: number;
/** Whether the agent is verified on-chain */
verified: boolean;
/** Agent display name */
name?: string;
/** Capabilities tags */
capabilities?: string[];
/** Last verification timestamp */
lastVerified?: string;
}

/**
* Configuration for SATP agent trust verification
*/
export interface SATPConfig {
/**
* AgentFolio API base URL
* @default "https://api.agentfolio.bot"
*/
apiUrl?: string;

/**
* Minimum trust score required (0-100)
* Set to 0 to allow all agents but still annotate requests with trust data
* @default 0
*/
minTrustScore?: number;

/**
* Require on-chain verification
* @default false
*/
requireVerified?: boolean;

/**
* Header name for agent identity
* @default "x-agent-id"
*/
agentIdHeader?: string;

/**
* Behavior when agent identity is missing from request
* - "reject": Return 401
* - "allow": Continue without trust data
* @default "allow"
*/
onMissing?: "reject" | "allow";

/**
* Cache TTL in milliseconds for trust score lookups
* @default 300000 (5 minutes)
*/
cacheTtlMs?: number;
}

/**
* SATP Agent Trust Provider
*
* Verifies agent identity and trust scores via AgentFolio/SATP.
* Can be used standalone or composed with other auth providers.
*
* @example
* ```typescript
* import { MCPServer, SATPProvider } from "mcp-framework";
*
* const server = new MCPServer({
* auth: {
* provider: new SATPProvider({
* minTrustScore: 50,
* requireVerified: true,
* }),
* },
* });
* ```
*/
export class SATPProvider implements AuthProvider {
private config: Required<SATPConfig>;
private cache: Map<string, { result: AgentTrustResult; expiry: number }> =
new Map();

constructor(config: SATPConfig = {}) {
this.config = {
apiUrl: config.apiUrl ?? "https://api.agentfolio.bot",
minTrustScore: config.minTrustScore ?? 0,
requireVerified: config.requireVerified ?? false,
agentIdHeader: config.agentIdHeader ?? "x-agent-id",
onMissing: config.onMissing ?? "allow",
cacheTtlMs: config.cacheTtlMs ?? 300_000,
};
}

async authenticate(req: IncomingMessage): Promise<boolean | AuthResult> {
const agentId = this.extractAgentId(req);

if (!agentId) {
return this.config.onMissing === "allow" ? { data: { agentTrust: null } } : false;
}

const trust = await this.queryTrust(agentId);

if (!trust) {
return this.config.onMissing === "allow" ? { data: { agentTrust: null } } : false;
}

// Check minimum trust score
if (trust.trustScore < this.config.minTrustScore) {
return false;
}

// Check verification requirement
if (this.config.requireVerified && !trust.verified) {
return false;
}

return {
data: {
agentTrust: trust,
},
};
}

getAuthError(): { status: number; message: string; headers?: Record<string, string> } {
return {
status: 403,
message: "Agent trust verification failed",
headers: {
"X-Trust-Required": `min-score=${this.config.minTrustScore}`,
},
};
}

/**
* Extract agent ID from request headers or MCP metadata
*/
private extractAgentId(req: IncomingMessage): string | null {
// Check custom header first
const headerValue = req.headers[this.config.agentIdHeader];
if (headerValue) {
return Array.isArray(headerValue) ? headerValue[0] : headerValue;
}

// Check Authorization header for agent token
const auth = req.headers.authorization;
if (auth?.startsWith("Agent ")) {
return auth.slice(6).trim();
}

return null;
}

/**
* Query AgentFolio API for agent trust data with caching
*/
private async queryTrust(agentId: string): Promise<AgentTrustResult | null> {
// Check cache
const cached = this.cache.get(agentId);
if (cached && cached.expiry > Date.now()) {
return cached.result;
}

try {
const response = await fetch(
`${this.config.apiUrl}/v1/agents/${encodeURIComponent(agentId)}/trust`,
{
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": "mcp-framework-satp/1.0",
},
signal: AbortSignal.timeout(5000),
}
);

if (!response.ok) {
return null;
}

const data = (await response.json()) as AgentTrustResult;
const result: AgentTrustResult = {
agentId: data.agentId ?? agentId,
trustScore: data.trustScore ?? 0,
verified: data.verified ?? false,
name: data.name,
capabilities: data.capabilities,
lastVerified: data.lastVerified,
};

// Cache result
this.cache.set(agentId, {
result,
expiry: Date.now() + this.config.cacheTtlMs,
});

return result;
} catch {
return null;
}
}

/**
* Clear the trust score cache
*/
clearCache(): void {
this.cache.clear();
}
}