feat(builder): rate limit per identity instead of per process
The limiter was a module-level singleton, so one busy caller throttled every other caller on the box and a shared daily budget was trivially exhausted. Buckets are now keyed by authenticated identity, and keyed centrally so reconnecting does not hand back a clean bucket.
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
} from '@apexfusion/vector-mcp-shared/config';
|
||||
export { deriveNftAssetName };
|
||||
import { safetyLayer } from './safety.js';
|
||||
import { rateLimiter } from './rate-limiter.js';
|
||||
import { limiterFor } from './rate-limiter.js';
|
||||
|
||||
// Registry constants - agent-registry v2 (audited, compliant)
|
||||
// Source blueprint: vector-ai-agents/agent-registry/deploy/agent-registry/plutus.json
|
||||
@@ -156,19 +156,21 @@ async function getRegistryAddress() {
|
||||
return _registryAddress;
|
||||
}
|
||||
|
||||
// ─── Rate limit wrapper ──────────────────────────────────────────────────────
|
||||
|
||||
function checkRateLimit() {
|
||||
const rateCheck = rateLimiter.check();
|
||||
if (!rateCheck.allowed) {
|
||||
return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Tool Registration ───────────────────────────────────────────────────────
|
||||
|
||||
export function registerAgentNetworkTools(server) {
|
||||
export function registerAgentNetworkTools(server, identity) {
|
||||
const rateLimiter = limiterFor(identity);
|
||||
|
||||
// Rate limit wrapper - defined inside the register function (not at module
|
||||
// scope) so this closes over the per-identity `rateLimiter` above rather
|
||||
// than a module-wide global.
|
||||
function checkRateLimit() {
|
||||
const rateCheck = rateLimiter.check();
|
||||
if (!rateCheck.allowed) {
|
||||
return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// vector_get_agent_profile (read-only - no mnemonic needed)
|
||||
server.tool(
|
||||
|
||||
@@ -24,4 +24,23 @@ export class RateLimiter {
|
||||
}
|
||||
|
||||
const RATE_LIMIT_PER_MINUTE = parseInt(process.env.VECTOR_RATE_LIMIT_PER_MINUTE || '60');
|
||||
export const rateLimiter = new RateLimiter(RATE_LIMIT_PER_MINUTE);
|
||||
|
||||
// One bucket per identity. A process-global limiter meant one busy caller
|
||||
// throttled everyone on the box; keying by identity fixes that. The registry
|
||||
// is keyed rather than per-connection so a caller cannot reset its own budget
|
||||
// by reconnecting.
|
||||
const limiters = new Map<string, RateLimiter>();
|
||||
|
||||
export function limiterFor(identity: string): RateLimiter {
|
||||
let limiter = limiters.get(identity);
|
||||
if (!limiter) {
|
||||
limiter = new RateLimiter(RATE_LIMIT_PER_MINUTE);
|
||||
limiters.set(identity, limiter);
|
||||
}
|
||||
return limiter;
|
||||
}
|
||||
|
||||
/** Test-only. Clears every bucket. */
|
||||
export function resetLimiters(): void {
|
||||
limiters.clear();
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
VECTOR_OGMIOS_URL, VECTOR_SUBMIT_URL, VECTOR_KOIOS_URL, VECTOR_EXPLORER_URL, explorerTxLink,
|
||||
} from '@apexfusion/vector-mcp-shared/config';
|
||||
import { safetyLayer } from './safety.js';
|
||||
import { rateLimiter } from './rate-limiter.js';
|
||||
import { limiterFor } from './rate-limiter.js';
|
||||
|
||||
// Agent Registry policy ID (shared across all modules)
|
||||
const AGENT_REGISTRY_POLICY = process.env.AGENT_REGISTRY_POLICY || 'be1a0a2912da180757ed3cd61b56bb8eab0188c19dc3c0e3912d2c01';
|
||||
@@ -85,14 +85,6 @@ function lovelaceToApex(lovelace: number | bigint): string {
|
||||
return (Number(BigInt(String(lovelace))) / 1_000_000).toFixed(6);
|
||||
}
|
||||
|
||||
function checkRateLimit() {
|
||||
const rateCheck = rateLimiter.check();
|
||||
if (!rateCheck.allowed) {
|
||||
return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function scriptHashToAddress(hash: string): string {
|
||||
return credentialToAddress('Mainnet', { type: 'Script', hash });
|
||||
}
|
||||
@@ -263,7 +255,20 @@ function parseEndorsementDatum(datumCbor: string): any | null {
|
||||
|
||||
// ─── Tool Registration ──────────────────────────────────────────────────────
|
||||
|
||||
export function registerSelfImprovementTools(server) {
|
||||
export function registerSelfImprovementTools(server, identity) {
|
||||
const rateLimiter = limiterFor(identity);
|
||||
|
||||
// Rate limit wrapper - defined inside the register function (not at module
|
||||
// scope, where it used to live alongside the other helpers above) so this
|
||||
// closes over the per-identity `rateLimiter` above rather than a
|
||||
// module-wide global.
|
||||
function checkRateLimit() {
|
||||
const rateCheck = rateLimiter.check();
|
||||
if (!rateCheck.allowed) {
|
||||
return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── vector_self_improvement_browse (read-only) ───────────────────────────────
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
VECTOR_OGMIOS_URL, VECTOR_SUBMIT_URL, VECTOR_KOIOS_URL, VECTOR_EXPLORER_URL, explorerTxLink,
|
||||
} from '@apexfusion/vector-mcp-shared/config';
|
||||
import { safetyLayer } from './safety.js';
|
||||
import { rateLimiter } from './rate-limiter.js';
|
||||
import { limiterFor } from './rate-limiter.js';
|
||||
import { registerAgentNetworkTools } from './agent-network.js';
|
||||
import { registerSelfImprovementTools } from './self-improvement.js';
|
||||
import type {
|
||||
@@ -473,7 +473,8 @@ export async function interactWithContract(
|
||||
}
|
||||
|
||||
// Register all Vector MCP tools
|
||||
export function registerVectorTools(server: McpServer) {
|
||||
export function registerVectorTools(server: McpServer, identity: string) {
|
||||
const rateLimiter = limiterFor(identity);
|
||||
|
||||
// vector_get_balance - Get balance for any address
|
||||
server.tool(
|
||||
@@ -1286,8 +1287,8 @@ Funds ${actionVerb} the script address.
|
||||
|
||||
// Agent network tools (register, discover, message, profile) live in agent-network.ts
|
||||
// to isolate C (Cardano WASM) imports from tsc's complex type inference
|
||||
registerAgentNetworkTools(server);
|
||||
registerAgentNetworkTools(server, identity);
|
||||
|
||||
// Self-Improvement Module tools (Module 6)
|
||||
registerSelfImprovementTools(server);
|
||||
registerSelfImprovementTools(server, identity);
|
||||
}
|
||||
|
||||
BIN
packages/builder/test/rate-limiter.test.ts
Normal file
BIN
packages/builder/test/rate-limiter.test.ts
Normal file
Binary file not shown.
Reference in New Issue
Block a user