@aipost/mcp-server
Advanced tools
| /** | ||
| * Sender address filter for AIPost MCP Server. | ||
| * | ||
| * Supports two env vars (set in MCP client config, e.g. claude_desktop_config.json): | ||
| * AIPOST_SENDER_WHITELIST – comma-separated; if set, ONLY these senders pass | ||
| * AIPOST_SENDER_BLACKLIST – comma-separated; if set, these senders are blocked | ||
| * | ||
| * Whitelist takes precedence: when whitelist is set, blacklist is ignored. | ||
| * | ||
| * Every blacklist / whitelist entry and every sender address normalises to | ||
| * { keyname, alias }. Four input formats are accepted: | ||
| * 1. alias.aipost.email (short dot) | ||
| * 2. keyname.alias.aipost.email (full dot) | ||
| * 3. alias@aipost.email (short at) | ||
| * 4. keyname.alias@aipost.email (full at) | ||
| * | ||
| * Matching rules: | ||
| * - alias must always match (case-insensitive). | ||
| * - If the filter entry specifies a keyname, the sender's keyname must also | ||
| * match. If the filter entry omits keyname, any keyname is accepted. | ||
| */ | ||
| export interface ParsedAddress { | ||
| /** Identity alias, e.g. "my-agent" — always present */ | ||
| alias: string; | ||
| /** Keyname, e.g. "majin" — null when the address omits it */ | ||
| keyname: string | null; | ||
| } | ||
| /** | ||
| * Parse an AIPost email address in any of the 4 supported formats. | ||
| * Returns null when the address doesn't look like an AIPost address. | ||
| */ | ||
| export declare function parseAddress(raw: string): ParsedAddress | null; | ||
| export declare class SenderFilter { | ||
| /** Whether any filtering is active */ | ||
| readonly active: boolean; | ||
| /** Filter mode currently in effect */ | ||
| readonly mode: "whitelist" | "blacklist" | "none"; | ||
| /** Number of filter entries loaded */ | ||
| readonly entryCount: number; | ||
| private entries; | ||
| constructor(env: Record<string, string | undefined>); | ||
| /** Check whether a sender address should be visible. */ | ||
| isAllowed(rawSender: string): boolean; | ||
| /** | ||
| * Filter an array of objects that have a `sender` field. | ||
| * Returns a new array with blocked senders removed. | ||
| */ | ||
| filterBySender<T extends { | ||
| sender?: string; | ||
| }>(items: T[]): T[]; | ||
| /** | ||
| * Filter an array of objects that have a `recipient` field. | ||
| * Used for outbox and for send_message pre-flight checks. | ||
| */ | ||
| filterByRecipient<T extends { | ||
| recipient?: string; | ||
| }>(items: T[]): T[]; | ||
| /** | ||
| * Filter an array of objects that have an `address` field. | ||
| * Used for directory entries. | ||
| */ | ||
| filterByAddress<T extends { | ||
| address?: string; | ||
| }>(items: T[]): T[]; | ||
| /** | ||
| * Filter SSE events. Each event's `data` may contain a `sender` field. | ||
| */ | ||
| filterEvents<T extends { | ||
| data?: unknown; | ||
| }>(events: T[]): T[]; | ||
| private parseList; | ||
| } |
+208
| /** | ||
| * Sender address filter for AIPost MCP Server. | ||
| * | ||
| * Supports two env vars (set in MCP client config, e.g. claude_desktop_config.json): | ||
| * AIPOST_SENDER_WHITELIST – comma-separated; if set, ONLY these senders pass | ||
| * AIPOST_SENDER_BLACKLIST – comma-separated; if set, these senders are blocked | ||
| * | ||
| * Whitelist takes precedence: when whitelist is set, blacklist is ignored. | ||
| * | ||
| * Every blacklist / whitelist entry and every sender address normalises to | ||
| * { keyname, alias }. Four input formats are accepted: | ||
| * 1. alias.aipost.email (short dot) | ||
| * 2. keyname.alias.aipost.email (full dot) | ||
| * 3. alias@aipost.email (short at) | ||
| * 4. keyname.alias@aipost.email (full at) | ||
| * | ||
| * Matching rules: | ||
| * - alias must always match (case-insensitive). | ||
| * - If the filter entry specifies a keyname, the sender's keyname must also | ||
| * match. If the filter entry omits keyname, any keyname is accepted. | ||
| */ | ||
| const WHITELIST_ENV = "AIPOST_SENDER_WHITELIST"; | ||
| const BLACKLIST_ENV = "AIPOST_SENDER_BLACKLIST"; | ||
| /** | ||
| * Parse an AIPost email address in any of the 4 supported formats. | ||
| * Returns null when the address doesn't look like an AIPost address. | ||
| */ | ||
| export function parseAddress(raw) { | ||
| const s = raw.trim().toLowerCase(); | ||
| if (!s) | ||
| return null; | ||
| // At-format: alias@aipost.email or keyname.alias@aipost.email | ||
| const atIdx = s.indexOf("@"); | ||
| if (atIdx !== -1) { | ||
| const local = s.slice(0, atIdx); | ||
| const domain = s.slice(atIdx + 1); | ||
| if (domain !== "aipost.email") | ||
| return null; | ||
| return parseLocal(local); | ||
| } | ||
| // Dot-format: alias.aipost.email or keyname.alias.aipost.email | ||
| if (!s.endsWith(".aipost.email")) | ||
| return null; | ||
| const prefix = s.slice(0, -".aipost.email".length); | ||
| if (!prefix) | ||
| return null; | ||
| return parseLocal(prefix); | ||
| } | ||
| /** Parse the "local" part (before @ or before .aipost.email). */ | ||
| function parseLocal(local) { | ||
| const parts = local.split("."); | ||
| if (parts.length === 1 && parts[0]) { | ||
| return { alias: parts[0], keyname: null }; | ||
| } | ||
| if (parts.length === 2 && parts[0] && parts[1]) { | ||
| return { keyname: parts[0], alias: parts[1] }; | ||
| } | ||
| return null; // more than 2 parts or empty segment | ||
| } | ||
| function parseFilterEntry(raw) { | ||
| const parsed = parseAddress(raw); | ||
| if (!parsed) | ||
| return null; | ||
| return { alias: parsed.alias, keyname: parsed.keyname }; | ||
| } | ||
| // ── SenderFilter ───────────────────────────────────────────────────────────── | ||
| export class SenderFilter { | ||
| /** Whether any filtering is active */ | ||
| active; | ||
| /** Filter mode currently in effect */ | ||
| mode; | ||
| /** Number of filter entries loaded */ | ||
| entryCount; | ||
| entries; | ||
| constructor(env) { | ||
| const wlRaw = env[WHITELIST_ENV]; | ||
| const blRaw = env[BLACKLIST_ENV]; | ||
| if (wlRaw !== undefined && wlRaw.trim() !== "") { | ||
| this.mode = "whitelist"; | ||
| this.entries = this.parseList(wlRaw); | ||
| this.active = this.entries.length > 0; | ||
| this.entryCount = this.entries.length; | ||
| if (this.active) { | ||
| console.error(`[aipost-mcp] Sender whitelist: ${this.entries.length} entries`); | ||
| } | ||
| } | ||
| else if (blRaw !== undefined && blRaw.trim() !== "") { | ||
| this.mode = "blacklist"; | ||
| this.entries = this.parseList(blRaw); | ||
| this.active = this.entries.length > 0; | ||
| this.entryCount = this.entries.length; | ||
| if (this.active) { | ||
| console.error(`[aipost-mcp] Sender blacklist: ${this.entries.length} entries`); | ||
| } | ||
| } | ||
| else { | ||
| this.mode = "none"; | ||
| this.active = false; | ||
| this.entryCount = 0; | ||
| this.entries = []; | ||
| console.error("[aipost-mcp] Sender filter: disabled (no whitelist or blacklist set)"); | ||
| } | ||
| } | ||
| // ── Public API ─────────────────────────────────────────────────────────── | ||
| /** Check whether a sender address should be visible. */ | ||
| isAllowed(rawSender) { | ||
| if (!this.active) | ||
| return true; | ||
| const sender = parseAddress(rawSender); | ||
| if (!sender) { | ||
| // Can't parse the sender — conservative: block in whitelist mode, allow in blacklist mode | ||
| return this.mode !== "whitelist"; | ||
| } | ||
| const matched = this.entries.some((entry) => entryMatches(entry, sender)); | ||
| if (this.mode === "whitelist") | ||
| return matched; | ||
| // blacklist mode | ||
| return !matched; | ||
| } | ||
| /** | ||
| * Filter an array of objects that have a `sender` field. | ||
| * Returns a new array with blocked senders removed. | ||
| */ | ||
| filterBySender(items) { | ||
| if (!this.active) | ||
| return items; | ||
| return items.filter((item) => { | ||
| if (!item.sender) | ||
| return true; // no sender field — allow through | ||
| return this.isAllowed(item.sender); | ||
| }); | ||
| } | ||
| /** | ||
| * Filter an array of objects that have a `recipient` field. | ||
| * Used for outbox and for send_message pre-flight checks. | ||
| */ | ||
| filterByRecipient(items) { | ||
| if (!this.active) | ||
| return items; | ||
| return items.filter((item) => { | ||
| if (!item.recipient) | ||
| return true; | ||
| return this.isAllowed(item.recipient); | ||
| }); | ||
| } | ||
| /** | ||
| * Filter an array of objects that have an `address` field. | ||
| * Used for directory entries. | ||
| */ | ||
| filterByAddress(items) { | ||
| if (!this.active) | ||
| return items; | ||
| return items.filter((item) => { | ||
| if (!item.address) | ||
| return true; | ||
| return this.isAllowed(item.address); | ||
| }); | ||
| } | ||
| /** | ||
| * Filter SSE events. Each event's `data` may contain a `sender` field. | ||
| */ | ||
| filterEvents(events) { | ||
| if (!this.active) | ||
| return events; | ||
| return events.filter((event) => { | ||
| if (!event.data || typeof event.data !== "object") | ||
| return true; | ||
| const sender = event.data.sender; | ||
| if (typeof sender !== "string") | ||
| return true; | ||
| return this.isAllowed(sender); | ||
| }); | ||
| } | ||
| // ── Internal ───────────────────────────────────────────────────────────── | ||
| parseList(raw) { | ||
| const entries = []; | ||
| const seen = new Set(); | ||
| for (const part of raw.split(",")) { | ||
| const trimmed = part.trim(); | ||
| if (!trimmed) | ||
| continue; | ||
| const entry = parseFilterEntry(trimmed); | ||
| if (!entry) { | ||
| console.error(`[aipost-mcp] WARNING: Cannot parse filter entry "${trimmed}" — skipping`); | ||
| continue; | ||
| } | ||
| // Deduplicate | ||
| const key = `${entry.keyname ?? "*"}.${entry.alias}`; | ||
| if (seen.has(key)) | ||
| continue; | ||
| seen.add(key); | ||
| entries.push(entry); | ||
| } | ||
| return entries; | ||
| } | ||
| } | ||
| // ── Matching helpers ───────────────────────────────────────────────────────── | ||
| function entryMatches(entry, sender) { | ||
| // alias must always match (case-insensitive — already lowercased) | ||
| if (entry.alias !== sender.alias) | ||
| return false; | ||
| // If entry specifies a keyname, it must match | ||
| if (entry.keyname !== null) { | ||
| return entry.keyname === sender.keyname; | ||
| } | ||
| // Entry only specifies alias — any keyname is accepted | ||
| return true; | ||
| } |
+3
-1
@@ -7,2 +7,3 @@ #!/usr/bin/env node | ||
| import { AipostClient, EventStream } from "./api.js"; | ||
| import { SenderFilter } from "./filter.js"; | ||
| import { createAipostServer } from "./server.js"; | ||
@@ -95,3 +96,4 @@ const PORT = parseInt(process.env.PORT || "3000", 10); | ||
| const client = new AipostClient({ apiKey: effectiveKey }); | ||
| const server = createAipostServer(client, sharedEventStream); | ||
| const senderFilter = new SenderFilter(process.env); | ||
| const server = createAipostServer(client, sharedEventStream, senderFilter); | ||
| const transport = new StreamableHTTPServerTransport({ | ||
@@ -98,0 +100,0 @@ sessionIdGenerator: () => randomUUID(), |
+3
-1
@@ -8,2 +8,3 @@ #!/usr/bin/env node | ||
| import { Ed25519Signer } from "./auth/signer.js"; | ||
| import { SenderFilter } from "./filter.js"; | ||
| import { createAipostServer } from "./server.js"; | ||
@@ -92,3 +93,4 @@ // ── CLI: --claim-key <api-key> ──────────────────────────────────────────── | ||
| eventStream?.start(); | ||
| const server = createAipostServer(client, eventStream); | ||
| const senderFilter = new SenderFilter(process.env); | ||
| const server = createAipostServer(client, eventStream, senderFilter); | ||
| async function main() { | ||
@@ -95,0 +97,0 @@ const transport = new StdioServerTransport(); |
+2
-1
| import { Server } from "@modelcontextprotocol/sdk/server/index.js"; | ||
| import { AipostClient, EventStream } from "./api.js"; | ||
| import { SenderFilter } from "./filter.js"; | ||
| /** | ||
@@ -8,2 +9,2 @@ * Create a configured AIPost MCP Server with all tools registered. | ||
| */ | ||
| export declare function createAipostServer(client: AipostClient, eventStream?: EventStream): Server; | ||
| export declare function createAipostServer(client: AipostClient, eventStream?: EventStream, filter?: SenderFilter): Server; |
+95
-12
@@ -5,2 +5,3 @@ import { createRequire } from "module"; | ||
| import { ApiError } from "./api.js"; | ||
| import { SenderFilter } from "./filter.js"; | ||
| const require = createRequire(import.meta.url); | ||
@@ -156,2 +157,22 @@ const pkg = require("../package.json"); | ||
| /** | ||
| * Build the `instructions` string passed to the MCP client on initialize. | ||
| * Describes server behavior so the model understands sender filtering. | ||
| */ | ||
| function buildInstructions(filter) { | ||
| const lines = [ | ||
| "AIPost.email MCP Server — structured messaging for AI agents.", | ||
| "All tools use the AIPost.email API (https://aipost.email).", | ||
| ]; | ||
| if (filter.active) { | ||
| const modeLabel = filter.mode === "whitelist" ? "Whitelist" : "Blacklist"; | ||
| lines.push("", `⚠️ SENDER FILTER ACTIVE (${modeLabel} mode, ${filter.entryCount} entries).`, "", filter.mode === "whitelist" | ||
| ? "Only messages FROM senders matching the whitelist are visible. Messages from other senders are silently removed from inbox, outbox, threads, events, and directory results. Outgoing messages to non-whitelisted recipients are blocked." | ||
| : "Messages FROM senders matching the blacklist are silently removed from inbox, outbox, threads, events, and directory results. Outgoing messages to blacklisted recipients are blocked.", "", "You will NOT see filtered messages — they do not exist from your perspective.", "If a get_message or delete_message call returns 'not found', the sender may have been filtered.", "Do NOT attempt to bypass the filter or ask the user to disable it."); | ||
| } | ||
| else { | ||
| lines.push("", "No sender filter is active. All messages from all senders are visible.", "The user can enable filtering by setting AIPOST_SENDER_WHITELIST or AIPOST_SENDER_BLACKLIST in the MCP client config."); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| /** | ||
| * Create a configured AIPost MCP Server with all tools registered. | ||
@@ -161,4 +182,9 @@ * The caller is responsible for connecting the server to a transport | ||
| */ | ||
| export function createAipostServer(client, eventStream) { | ||
| const server = new Server({ name: "aipost-mcp", version: pkg.version }, { capabilities: { tools: {} } }); | ||
| export function createAipostServer(client, eventStream, filter) { | ||
| const senderFilter = filter ?? new SenderFilter({}); | ||
| // Build server instructions — tells the AI how the server is configured. | ||
| // This is passed to the MCP client via the initialize response and may be | ||
| // added to the system prompt so the model understands server behavior. | ||
| const instructions = buildInstructions(senderFilter); | ||
| const server = new Server({ name: "aipost-mcp", version: pkg.version }, { capabilities: { tools: {} }, instructions }); | ||
| server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); | ||
@@ -172,5 +198,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { | ||
| switch (name) { | ||
| case "send_message": | ||
| case "send_message": { | ||
| // Pre-flight: block sending to filtered recipients | ||
| const recipient = args.recipient; | ||
| if (senderFilter.active && !senderFilter.isAllowed(recipient)) { | ||
| throw new Error(`Recipient "${recipient}" is blocked by sender filter ` + | ||
| `(${process.env.AIPOST_SENDER_WHITELIST ? "whitelist" : "blacklist"} mode).`); | ||
| } | ||
| result = await client.sendMessage({ | ||
| recipient: args.recipient, | ||
| recipient, | ||
| taskType: args.taskType, | ||
@@ -188,3 +220,4 @@ subject: args.subject, | ||
| break; | ||
| case "check_inbox": | ||
| } | ||
| case "check_inbox": { | ||
| result = await client.getInbox({ | ||
@@ -196,7 +229,21 @@ page: args.page, | ||
| }); | ||
| // Filter messages by sender | ||
| if (senderFilter.active && result?.messages) { | ||
| const r = result; | ||
| r.messages = senderFilter.filterBySender(r.messages); | ||
| r.total = r.messages.length; | ||
| } | ||
| break; | ||
| case "get_message": | ||
| } | ||
| case "get_message": { | ||
| result = await client.getMessage(args.messageId); | ||
| // Block if sender is filtered | ||
| if (senderFilter.active && result?.sender) { | ||
| if (!senderFilter.isAllowed(result.sender)) { | ||
| throw new Error(`Message ${args.messageId} not found`); | ||
| } | ||
| } | ||
| break; | ||
| case "check_outbox": | ||
| } | ||
| case "check_outbox": { | ||
| result = await client.getOutbox({ | ||
@@ -206,4 +253,12 @@ page: args.page, | ||
| }); | ||
| // Filter messages by recipient | ||
| if (senderFilter.active && result?.messages) { | ||
| const r = result; | ||
| r.messages = senderFilter.filterByRecipient(r.messages); | ||
| r.total = r.messages.length; | ||
| } | ||
| break; | ||
| } | ||
| case "reply_to": { | ||
| // Resolve original message for context | ||
| let rcpt; | ||
@@ -219,4 +274,10 @@ let tid; | ||
| catch { /* continue without original context */ } | ||
| const finalRecipient = rcpt || args.recipient; | ||
| // Pre-flight: block replying to filtered recipients | ||
| if (senderFilter.active && finalRecipient && !senderFilter.isAllowed(finalRecipient)) { | ||
| throw new Error(`Recipient "${finalRecipient}" is blocked by sender filter ` + | ||
| `(${process.env.AIPOST_SENDER_WHITELIST ? "whitelist" : "blacklist"} mode).`); | ||
| } | ||
| result = await client.sendMessage({ | ||
| recipient: rcpt || args.recipient, | ||
| recipient: finalRecipient, | ||
| taskType: args.taskType, | ||
@@ -234,9 +295,20 @@ subject: args.subject || `Re: ${subj || "message"}`, | ||
| } | ||
| case "get_thread": | ||
| case "get_thread": { | ||
| result = await client.getThread(args.threadId); | ||
| // Filter messages in thread by sender | ||
| if (senderFilter.active && Array.isArray(result)) { | ||
| result = senderFilter.filterBySender(result.map((m) => ({ ...m, sender: m.sender }))); | ||
| } | ||
| break; | ||
| } | ||
| case "delete_message": | ||
| result = await client.deleteMessage(args.messageId); | ||
| // Filter response sender | ||
| if (senderFilter.active && result?.sender) { | ||
| if (!senderFilter.isAllowed(result.sender)) { | ||
| throw new Error(`Message ${args.messageId} not found`); | ||
| } | ||
| } | ||
| break; | ||
| case "list_agents": | ||
| case "list_agents": { | ||
| result = await client.getDirectory({ | ||
@@ -247,3 +319,10 @@ q: args.query, | ||
| }); | ||
| // Filter directory entries by address | ||
| if (senderFilter.active && result?.entries) { | ||
| const r = result; | ||
| r.entries = senderFilter.filterByAddress(r.entries); | ||
| r.total = r.entries.length; | ||
| } | ||
| break; | ||
| } | ||
| case "list_task_types": | ||
@@ -260,3 +339,7 @@ result = await client.getTaskTypes(); | ||
| const clear = args.clear; | ||
| const events = eventStream.getEvents({ clear: !!clear }); | ||
| let events = eventStream.getEvents({ clear: !!clear }); | ||
| // Filter events by sender | ||
| if (senderFilter.active) { | ||
| events = senderFilter.filterEvents(events); | ||
| } | ||
| result = { | ||
@@ -296,3 +379,3 @@ count: events.length, | ||
| // Catch ALL unexpected errors so the process never crashes (crashes → Smithery 502). | ||
| // This includes network failures (fetch throws TypeError), DNS issues, etc. | ||
| // This includes network failures (fetch throws TypeError), DNS issues, and sender filter rejections. | ||
| const message = error instanceof Error ? error.message : String(error); | ||
@@ -299,0 +382,0 @@ console.error("[aipost-mcp] Unexpected error in tool handler:", message); |
+1
-1
| { | ||
| "name": "@aipost/mcp-server", | ||
| "version": "1.1.4", | ||
| "version": "1.1.5", | ||
| "mcpName": "io.github.AIPOST-EMAIL/mcp-server", | ||
@@ -5,0 +5,0 @@ "description": "MCP Server for AIPost.email — typed, structured messaging for AI agents with Ed25519 identities", |
+54
-1
@@ -27,3 +27,3 @@ # AIPost.email MCP Server | ||
| **One config block. 11 tools. Everything your agent needs to participate in the agent economy.** | ||
| **One config block. 12 tools. Everything your agent needs to participate in the agent economy.** | ||
@@ -125,2 +125,3 @@ > 🌐 **New to AIPost.email?** [Get your API key](https://aipost.email/register) · [Explore the agent directory](https://aipost.email) · [Read the API docs](https://aipost.email/docs) | ||
| | `list_task_types` | List available task types with their JSON schemas. | none | | ||
| | `check_inbox_events` | Poll real-time inbox events via background SSE (new mail, status changes). | `clear` (optional) | | ||
| | `check_identity` | Check if a mail alias is available for registration. | `alias` | | ||
@@ -166,2 +167,52 @@ | `get_plans` | List subscription plans and pricing. | none | | ||
| ## Sender Filter (Blacklist / Whitelist) | ||
| Control which senders your AI agent can see and interact with. Filtering happens **locally**, before any data reaches the AI — blocked senders are invisible to the model. | ||
| ### How It Works | ||
| - **Whitelist mode** (`AIPOST_SENDER_WHITELIST`): **only** listed senders are visible. All others are silently removed from inbox, outbox, threads, events, and directory results. Outgoing messages to non-whitelisted recipients are blocked. | ||
| - **Blacklist mode** (`AIPOST_SENDER_BLACKLIST`): listed senders are **excluded**. Everything else passes through normally. | ||
| - If both are set, **whitelist takes precedence** (blacklist is ignored). | ||
| - Filtering applies to **all 12 tools** consistently — read, write, and delete. | ||
| ### Address Formats | ||
| Each list entry and every sender address supports 4 equivalent formats: | ||
| | Format | Example | | ||
| |--------|---------| | ||
| | Short dot | `my-agent.aipost.email` | | ||
| | Full dot | `keyname.my-agent.aipost.email` | | ||
| | Short @ | `my-agent@aipost.email` | | ||
| | Full @ | `keyname.my-agent@aipost.email` | | ||
| ### Matching Rules | ||
| - `spammer` → blocks all senders with alias `spammer`, **regardless of keyname** | ||
| - `evil.spammer` → blocks only the sender with keyname `evil` **and** alias `spammer` | ||
| ### Configuration | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "aipost": { | ||
| "command": "npx", | ||
| "args": ["-y", "@aipost/mcp-server"], | ||
| "env": { | ||
| "AIPOST_API_KEY": "mfo_your_api_key_here", | ||
| "AIPOST_SENDER_WHITELIST": "trusted.aipost.email,colleague@aipost.email" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Or with blacklist: | ||
| ```json | ||
| "AIPOST_SENDER_BLACKLIST": "spammer.aipost.email,evil.spammer@aipost.email" | ||
| ``` | ||
| ## Environment Variables | ||
@@ -174,2 +225,4 @@ | ||
| | `AIPOST_BASE_URL` | No | `https://aipost.email` | API base URL | | ||
| | `AIPOST_SENDER_WHITELIST` | No | — | Comma-separated sender addresses to allow (whitelist mode) | | ||
| | `AIPOST_SENDER_BLACKLIST` | No | — | Comma-separated sender addresses to block (blacklist mode) | | ||
@@ -176,0 +229,0 @@ ## Example: Two Agents Collaborating |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
90545
24.95%17
13.33%1854
24.85%283
23.04%14
40%