ESC

Type to search...

Security

Security in EVOID operates at three layers: secrets management, pipeline enforcement, and AI data control. Each layer is independent. You can use one without the others.

1. Secrets Management

EVOID has minimal core dependencies and no built-in .env loader. This is deliberate. Secrets management is infrastructure, not runtime. Here’s how to handle it.

The Problem with .env Files

Every framework handles secrets differently. Some load .env automatically. Some require python-dotenv. Some expose secrets in config files. The result: secrets leak into git, get hardcoded, or get scattered across config files.

The EVOID Pattern: Environment Variables

The simplest approach: use os.environ directly:

import os
from evoid.config import config

DB_URL = os.environ["DATABASE_URL"]  # crashes if missing: intentional
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost")  # optional

app = config(
    service={"name": "my-api"},
    engines={"storage": "sqlite"},
)

Development: .env + python-dotenv

uv add python-dotenv
# main.py: load .env before anything else
from dotenv import load_dotenv
load_dotenv()

import os
from evoid.config import config

DB_URL = os.environ["DATABASE_URL"]
# .env (never commit this)
DATABASE_URL=sqlite:///dev.db
REDIS_URL=redis://localhost
SECRET_KEY=dev-secret-key
# .gitignore
.env
.env.*
!.env.example

Production: Environment Variables

# Docker
docker run -e DATABASE_URL="postgres://..." my-app

# Systemd
Environment=DATABASE_URL=postgres://...

# Kubernetes
env:
  - name: DATABASE_URL
    valueFrom:
      secretKeyRef:
        name: my-secrets
        key: database-url

Production: Secret Managers

import os

# AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager
# Fetch secrets at startup, inject as env vars

DB_URL = os.environ["DATABASE_URL"]  # injected by orchestrator

Config Files: Never Hardcode Secrets

# evoid.toml: safe (no secrets)
[service]
name = "my-api"

[engines]
storage = "sqlite"

# WRONG: never put secrets in config files
# [engines.redis]
# url = "redis://:password@localhost"  # ❌
# RIGHT: secrets from env, config from file
import os
from evoid.config import config

app = config(
    service={"name": "my-api"},
    engines={"storage": "sqlite"},
)

# Redis URL from environment
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost")

.env.example Template

Provide a template for developers:

# .env.example (commit this)
DATABASE_URL=sqlite:///dev.db
REDIS_URL=redis://localhost
SECRET_KEY=change-me-in-production
# Developer setup
cp .env.example .env
# Edit .env with local values

2. Pipeline Security

EVOID’s pipeline enforces security through Intent levels. The level you choose determines which security processors run.

Security by Level

LevelSecurity ProcessorsWhat Runs
ephemeralvalidateData shape check only
standardvalidateauthorizeShape check + permission check
criticalvalidateauthorizeauditprotectFull security stack

The protect Processor

The built-in protect processor provides:

  • Rate limiting: per-key request throttling
  • Circuit breaker: stops processing after repeated failures
from evoid import Intent, Level

# This Intent gets full protection
PAYMENT = Intent(
    name="process_payment",
    level=Level.CRITICAL,
    # Pipeline: validate → authorize → audit → protect → handler
    # Rate limit: 20 requests/minute
    # Circuit breaker: opens after 3 failures, half-open after 30s
)

The authorize Processor

The built-in authorize processor checks roles only if an auth engine is registered. If no auth engine is configured, the check is silently skipped — the Intent proceeds as if authorized.

If you need enforcement, register an auth provider. Without one, `authorize` is a no-op.
from evoid_auth import register_provider

async def my_auth(token: str) -> dict:
    user = await db.find_by_token(token)
    return {"user": user.name, "role": user.role}

register_provider("my_auth", my_auth)

Role hierarchy: admin(4) > editor(3) > viewer(2) > guest(1)

# This Intent requires editor role or higher
CREATE_POST = Intent(
    name="create_post",
    level=Level.STANDARD,
    metadata={"required_role": "editor"},
)

Plugin Security

Plugins follow EVOID’s security model:

  • Event hooks are read-only: hooks receive EventContext (frozen), never the mutable Context
  • Max 16 hooks per event: prevents hook flooding
  • 5s timeout per hook: prevents slow hooks from blocking the pipeline
  • Zero cost when no hooks: single dict length check
from evoid import on_event, Event

# This hook can READ the context, but NOT modify it
def log_execution(ctx):
    print(f"Executed: {ctx.intent_name}")  # read-only

on_event(Event.POST_EXECUTE, log_execution)
# ctx is EventContext: frozen, read-only
# You cannot write to ctx.state or ctx.deps

3. AI Data Control

When you expose Intents to AI agents via MCP, you control exactly what the AI can see and do.

The Problem

AI agents need to understand your system to interact with it. But you don’t want them seeing:

  • Internal implementation details
  • Sensitive business logic
  • Admin-only operations
  • Debug endpoints

The Solution: Visibility Control

EVOID uses mcp_visible in Intent metadata to control what AI agents see:

from evoid import Intent, Level

# Visible to AI agents: AI can discover and invoke this
GET_USER = Intent(
    name="get_user",
    level=Level.STANDARD,
    metadata={
        "description": "Get a user by their unique ID",
        "mcp_visible": True,  # ← AI agents can see this
    },
)

# Hidden from AI agents (default): AI cannot see this
DELETE_USER = Intent(
    name="delete_user",
    level=Level.CRITICAL,
    metadata={
        "description": "Permanently delete a user",
        # mcp_visible defaults to False: AI cannot see this
    },
)

# Internal hook: completely invisible
DEBUG_INTENT = Intent(
    name="internal_debug",
    level=Level.EPHEMERAL,
    # No description, no mcp_visible: invisible to everything
)

How It Works

from evoid.adapters.mcp import create_mcp_server

# Create MCP server: only visible Intents are exposed
server = create_mcp_server("my-api", visible_only=True)

# AI agent sees:
#   - get_user (mcp_visible=True)
#   - create_post (mcp_visible=True)
#
# AI agent does NOT see:
#   - delete_user (mcp_visible=False, default)
#   - internal_debug (no metadata)

Granular Control

Control visibility at multiple levels:

Per-Intent: mcp_visible

# Show to AI
Intent(name="get_user", metadata={"mcp_visible": True})

# Hide from AI (default)
Intent(name="admin_panel", metadata={})

Per-Field: Metadata Schema

Control which fields AI agents can see:

GET_USER = Intent(
    name="get_user",
    level=Level.STANDARD,
    metadata={
        "description": "Get a user by ID",
        "mcp_visible": True,
        # Only these fields are exported to AI:
        "user_id": 0,  # AI can see: "user_id is an integer"
        # Internal fields are hidden:
        "internal_flag": True,  # Not in schema export
    },
)

Per-Level: Pipeline Enforcement

Even if AI invokes an Intent, the pipeline still runs:

# AI agent calls get_user
# Pipeline: validate → authorize → handler
# The authorize step checks the AI agent's permissions
# If the AI agent doesn't have the right role → denied

# AI agent tries to call delete_user
# Can't even see it (mcp_visible=False)
# Even if it somehow called it → CRITICAL pipeline → full audit trail

What AI Agents See vs. Don’t See

DataVisible?How to Control
Intent nameYes (if mcp_visible)mcp_visible flag
Intent descriptionYesmetadata["description"]
Metadata fieldsOnly documented onesInclude in metadata dict
Pipeline configYesAlways exported (for AI understanding)
Handler codeNeverAI only sees the Intent, not the implementation
ctx.stateNeverContext is server-side only
ctx.depsNeverDependencies are server-side only
Other IntentsOnly if visibleEach Intent has its own mcp_visible

AI Agent Security Checklist

  • Mark only necessary Intents with mcp_visible=True
  • Use Level.STANDARD or Level.CRITICAL for AI-exposed Intents
  • Add descriptions so AI understands what each Intent does
  • Never expose admin/debug Intents to AI
  • Review export_schemas() output before deploying
  • Monitor AI agent invocations via audit logs (Level.CRITICAL)
# Audit what AI agents can see
from evoid import export_schemas

schemas = export_schemas()
for name, schema in schemas.items():
    print(f"{name}: {schema.description}")
    print(f"  Level: {schema.level}")
    print(f"  Pipeline: {schema.pipeline}")
    # Review before deploying to production

Summary

LayerWhat It ProtectsHow
SecretsCredentials, API keysEnvironment variables, never in config
PipelineRequest processingIntent levels → security processors
AI ControlData exposed to AImcp_visible flag, schema export