- Overview
- System Architecture
- Core Principles
- Component Diagrams
- Trait Hierarchy
- Data Flow
- Engine Isolation
- Stateless Design
- Capability Enforcement
- Configuration Resolution
- Output Envelopes
- MCP Integration
- Security Model
Plenum is a lightweight, agent-first database control CLI built in Rust. It provides a deterministic, least-privilege execution surface for AI agents to interact with databases safely.
Key architectural decisions:
- Stateless execution: No persistent connections between invocations
- Engine isolation: Each database engine is completely independent
- Capability-based security: Read-only by default, explicit flags for writes/DDL
- JSON-only output: Machine-parseable, no human-oriented features
- Vendor-specific SQL: No query abstraction layer
- Native drivers: tokio-postgres, mysql_async, rusqlite (NOT sqlx)
Plenum is structured as a library with thin CLI and MCP wrappers:
┌─────────────────────────────────────────────────────────────────┐
│ Plenum Library │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Error │ │ Output │ │ Capability │ │ Config │ │
│ │ Handling │ │ Envelopes │ │ Validation │ │ Mgmt │ │
│ └────────────┘ └────────────┘ └────────────┘ └──────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ DatabaseEngine Trait (stateless) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ SQLite │ │ PostgreSQL │ │ MySQL │ │
│ │ Engine │ │ Engine │ │ Engine │ │
│ │ (rusqlite) │ │ (tokio-pg) │ │(mysql_async)│ │
│ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ CLI Wrapper │ │ MCP Server │
│ (main.rs) │ │ (mcp.rs) │
│ │ │ │
│ • connect │ │ • connect tool │
│ • introspect │ │ • introspect │
│ • query │ │ • query tool │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
JSON stdout JSON-RPC stdio
- JSON-only output to stdout (no human-friendly formatting)
- Deterministic behavior (same input → same output)
- No interactive UX for execution (interactive config setup allowed)
- Explicit over implicit (no inferred values)
- SQL remains vendor-specific
- PostgreSQL SQL ≠ MySQL SQL ≠ SQLite SQL
- No compatibility layers or universal SQL
- Each engine handles its own quirks
- Read-only mode is the default
- Writes require
--allow-writeflag - DDL requires
--allow-ddlflag - Capability checks happen before execution
- No persistent database connections
- Each command invocation:
- Reads config from disk
- Opens connection
- Executes operation
- Closes connection
- Returns result
- No connection pooling across invocations
- No caching
- Each engine module is completely independent
- No shared SQL helpers across engines
- Native drivers (NOT sqlx) for maximum isolation
- Engine-specific behavior stays inside engine modules
src/
├── lib.rs # Public API exports
├── main.rs # CLI entry point
├── error.rs # PlenumError enum & Result type
├── output.rs # JSON envelope types
├── capability/
│ └── mod.rs # SQL categorization & validation
├── config/
│ └── mod.rs # Connection configuration management
└── engine/
├── mod.rs # DatabaseEngine trait & core types
├── sqlite/
│ └── mod.rs # SQLite implementation
├── postgres/
│ └── mod.rs # PostgreSQL implementation
└── mysql/
└── mod.rs # MySQL implementation
┌──────────────────────────────────────────────────┐
│ Plenum Binary │
│ ┌──────────────────────────────────────────┐ │
│ │ Plenum Library │ │
│ │ │ │
│ │ Core Deps: │ │
│ │ • serde, serde_json (serialization) │ │
│ │ • thiserror, anyhow (error handling) │ │
│ │ • clap (CLI) │ │
│ │ • dirs (config paths) │ │
│ │ • dialoguer (interactive UI) │ │
│ │ • tokio (async runtime) │ │
│ │ │ │
│ │ Engine Deps (feature-gated): │ │
│ │ • rusqlite (SQLite) │ │
│ │ • tokio-postgres (PostgreSQL) │ │
│ │ • mysql_async (MySQL) │ │
│ │ │ │
│ │ MCP Deps: │ │
│ │ • rmcp (MCP protocol) │ │
│ │ • schemars (JSON schemas) │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
The DatabaseEngine trait is the core abstraction. All three engines implement it identically:
pub trait DatabaseEngine {
/// Validate connection and return metadata
/// Opens connection, retrieves info, closes connection
fn validate_connection(config: &ConnectionConfig) -> Result<ConnectionInfo>;
/// Introspect database schema
/// Opens connection, queries schema, closes connection
fn introspect(
config: &ConnectionConfig,
schema_filter: Option<&str>
) -> Result<SchemaInfo>;
/// Execute query with capability constraints
/// Opens connection, validates capabilities, executes, closes
fn execute(
config: &ConnectionConfig,
query: &str,
caps: &Capabilities
) -> Result<QueryResult>;
}Key trait characteristics:
- All methods are static (no
&selfparameter) - All methods are stateless (no internal state)
- All methods take
&ConnectionConfigas input - Connections are opened and closed within each method
- No persistent connections between calls
DatabaseEngine (trait)
├── SqliteEngine::validate_connection()
├── SqliteEngine::introspect()
├── SqliteEngine::execute()
│
├── PostgresEngine::validate_connection()
├── PostgresEngine::introspect()
├── PostgresEngine::execute()
│
├── MysqlEngine::validate_connection()
├── MysqlEngine::introspect()
└── MysqlEngine::execute()
User/Agent Input
│
▼
┌─────────────┐
│ CLI Parsing │ (clap)
│ or MCP │
└─────────────┘
│
▼
┌─────────────────────┐
│ Config Resolution │ (config::resolve_connection)
│ 1. Explicit flags │ Priority order:
│ 2. Local config │ Explicit > Local > Global
│ 3. Global config │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Capability Building │ (Capabilities struct)
│ • allow_write │
│ • allow_ddl │
│ • max_rows │
│ • timeout_ms │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Engine Dispatch │ (match on DatabaseType)
│ match engine { │
│ SQLite => ... │
│ Postgres => ... │
│ MySQL => ... │
│ } │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Engine Execution │ (DatabaseEngine trait)
│ 1. Open connection │
│ 2. Execute op │
│ 3. Close conn │
│ 4. Return result │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Envelope Wrapping │ (SuccessEnvelope or ErrorEnvelope)
│ { │
│ ok: true/false, │
│ engine: "...", │
│ command: "...", │
│ data: {...}, │
│ meta: {...} │
│ } │
└─────────────────────┘
│
▼
JSON stdout
Query Input
│
▼
┌──────────────────────┐
│ SQL Preprocessing │
│ 1. Trim whitespace │
│ 2. Strip comments │
│ 3. Detect multi-stmt│
│ 4. Uppercase norm │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ SQL Categorization │ (engine-specific regex)
│ • ReadOnly: SELECT │
│ • Write: INSERT/... │
│ • DDL: CREATE/... │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ Capability Check │ BEFORE EXECUTION
│ ReadOnly → always OK│
│ Write → need write │
│ DDL → need ddl │
└──────────────────────┘
│
├──[FAIL]──────────────────┐
│ ▼
│ CapabilityViolation
│ Error
▼
┌──────────────────────┐
│ Query Execution │
│ • Open connection │
│ • Execute SQL │
│ • Parse results │
│ • Close connection │
└──────────────────────┘
│
├──[FAIL]──────────────────┐
│ ▼
│ QueryFailed
│ Error
▼
QueryResult
(success)
Each database engine is completely isolated from others:
-
No shared SQL helpers
- Each engine has its own SQL categorization logic
- PostgreSQL regex ≠ MySQL regex ≠ SQLite regex
- No common query parsing utilities
-
Native drivers only
- SQLite:
rusqlite(synchronous) - PostgreSQL:
tokio-postgres(async) - MySQL:
mysql_async(async) - NO sqlx (avoids abstraction leakage)
- SQLite:
-
Engine-specific modules
src/engine/ ├── mod.rs # Trait definition only ├── sqlite/ # SQLite-specific code ├── postgres/ # PostgreSQL-specific code └── mysql/ # MySQL-specific code -
Vendor-specific behavior
- MySQL implicit commits (DDL operations)
- PostgreSQL EXPLAIN ANALYZE execution
- SQLite dynamic typing
- Each engine handles its own quirks
Engines are feature-gated to reduce binary size:
[features]
default = ["sqlite", "postgres", "mysql"]
sqlite = ["dep:rusqlite"]
postgres = ["dep:tokio-postgres"]
mysql = ["dep:mysql_async"]Build with specific engines:
cargo build --no-default-features --features sqlite,postgresPlenum maintains no state between command invocations.
- Determinism: Same input → same output (no hidden state)
- Safety: No connection leaks or orphaned transactions
- Simplicity: No connection pool management
- Agent-friendly: Predictable behavior for AI agents
Every command follows this pattern:
pub fn execute_command(config: ConnectionConfig) -> Result<Output> {
// 1. Read config from disk (if needed)
let resolved_config = resolve_connection(Some("prod"))?;
// 2. Open connection
let mut conn = engine::connect(&resolved_config)?;
// 3. Execute operation
let result = operation(&mut conn)?;
// 4. Close connection (automatic via Drop)
drop(conn);
// 5. Return result
Ok(result)
}No persistent state:
- No
struct Serverwith connection fields - No global connection pool
- No cached query results
- No session state
Config files are NOT state:
- Config files store connection parameters (like bookmarks)
- Each invocation reads config fresh from disk
- Config is input data, not runtime state
Capability enforcement is the core security mechanism.
┌─────────────────────────────────────┐
│ DDL Capability │ ← Most privileged
│ • CREATE, DROP, ALTER, TRUNCATE │
│ • Implies Write capability │
│ • Requires --allow-ddl flag │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Write Capability │ ← Medium privilege
│ • INSERT, UPDATE, DELETE │
│ • Requires --allow-write flag │
│ • Does NOT imply DDL │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Read-Only (Default) │ ← Least privileged
│ • SELECT queries only │
│ • No flags needed │
│ • Always permitted │
└─────────────────────────────────────┘
pub fn validate_query(
sql: &str,
caps: &Capabilities,
engine: DatabaseType
) -> Result<QueryCategory> {
// 1. Preprocess SQL (strip comments, normalize)
let processed = preprocess_sql(sql)?;
// 2. Categorize query (engine-specific)
let category = categorize_query(&processed, engine)?;
// 3. Check capabilities BEFORE execution
match category {
QueryCategory::ReadOnly => Ok(category), // Always allowed
QueryCategory::Write => {
if caps.can_write() {
Ok(category)
} else {
Err(CapabilityViolation("Need --allow-write"))
}
}
QueryCategory::DDL => {
if caps.can_ddl() {
Ok(category)
} else {
Err(CapabilityViolation("Need --allow-ddl"))
}
}
}
}Each engine implements its own categorization logic:
PostgreSQL:
fn categorize_postgres(sql: &str) -> QueryCategory {
if sql.starts_with("CREATE ") || sql.starts_with("DROP ") { DDL }
else if sql.starts_with("INSERT ") || sql.starts_with("UPDATE ") { Write }
else if sql.starts_with("SELECT ") { ReadOnly }
else { DDL } // Fail-safe: unknown → most restrictive
}MySQL:
fn categorize_mysql(sql: &str) -> QueryCategory {
// MySQL-specific: LOCK TABLES causes implicit commit → DDL
if sql.starts_with("LOCK TABLES") { DDL }
else if sql.starts_with("CREATE ") { DDL }
// ... (similar to PostgreSQL)
}SQLite:
fn categorize_sqlite(sql: &str) -> QueryCategory {
// SQLite-specific: VACUUM, REINDEX are DDL
if sql.starts_with("VACUUM") || sql.starts_with("REINDEX") { DDL }
// ... (similar to PostgreSQL)
}- EXPLAIN queries: Strip EXPLAIN prefix, categorize underlying statement
- CTEs (WITH ... SELECT): Check final statement type
- Multi-statement queries: Rejected in MVP (safest approach)
- Comments: Stripped before categorization
- Unknown statements: Default to DDL (fail-safe)
Configuration is resolved in strict precedence order.
1. Explicit CLI flags (highest priority)
│
▼
--engine postgres --host db.example.com --port 5432
│
├──[if missing]─────────┐
│ ▼
│ 2. Local config
│ .plenum/config.json
│ │
│ ├──[if missing]─────────┐
│ │ ▼
│ │ 3. Global config
│ │ ~/.config/plenum/connections.json
│ │ │
│ │ ├──[if missing]─┐
│ │ │ ▼
│ │ │ ERROR
│ ▼ ▼ │
└────────────────────> Merge ◄───────────────────┘ │
│ │
▼ │
ConnectionConfig ◄──────────────────────────────┘
Local config (.plenum/config.json):
- Project-specific
- Team-shareable (can be committed to git)
- Located at project root
Global config (~/.config/plenum/connections.json):
- User-specific
- Cross-project
- Located in user config directory
{
"connections": {
"prod": {
"engine": "postgres",
"host": "db.example.com",
"port": 5432,
"user": "readonly",
"database": "production",
"password_env": "PROD_DB_PASSWORD"
},
"local": {
"engine": "sqlite",
"file": "./app.db"
}
},
"default": "local"
}Passwords can be stored as environment variable references:
{
"connections": {
"secure": {
"engine": "postgres",
"host": "db.example.com",
"user": "admin",
"password_env": "DB_PASSWORD", ← References env var
"database": "app"
}
}
}At runtime:
// Resolve password from environment
if let Some(env_var) = &stored.password_env {
config.password = Some(std::env::var(env_var)?);
}All Plenum output follows a strict JSON envelope format.
{
"ok": true,
"engine": "postgres",
"command": "query",
"data": {
"columns": ["id", "name"],
"rows": [
{"id": 1, "name": "Alice"}
]
},
"meta": {
"execution_ms": 42,
"rows_returned": 1
}
}Type definition:
pub struct SuccessEnvelope<T> {
pub ok: bool, // Always true
pub engine: String, // "postgres" | "mysql" | "sqlite"
pub command: String, // "connect" | "introspect" | "query"
pub data: T, // Command-specific data
pub meta: Metadata, // Execution metadata
}
pub struct Metadata {
pub execution_ms: u64,
pub rows_returned: Option<usize>,
}{
"ok": false,
"engine": "postgres",
"command": "query",
"error": {
"code": "CAPABILITY_VIOLATION",
"message": "DDL operations require --allow-ddl flag"
}
}Type definition:
pub struct ErrorEnvelope {
pub ok: bool, // Always false
pub engine: String, // Engine that produced error
pub command: String, // Command that failed
pub error: ErrorInfo, // Error details
}
pub struct ErrorInfo {
pub code: String, // Machine-readable error code
pub message: String, // Human-readable message
}| Code | Description | When It Occurs |
|---|---|---|
CAPABILITY_VIOLATION |
Operation blocked by capabilities | Write/DDL without appropriate flags |
CONNECTION_FAILED |
Database connection failed | Invalid credentials, unreachable host |
QUERY_FAILED |
Query execution failed | SQL syntax errors, missing tables |
INVALID_INPUT |
Malformed input | Missing required flags, invalid engine |
ENGINE_ERROR |
Engine-specific error | Database driver errors |
CONFIG_ERROR |
Configuration error | Missing config file, connection not found |
Guarantees:
- All output is valid JSON
- No logs or diagnostic text to stdout (errors go to stderr)
- Deterministic serialization (stable field order)
- No sensitive data in error messages (passwords never logged)
Validation:
- JSON Schema files in
schemas/directory - Snapshot tests ensure schema stability
- Integration tests verify cross-engine consistency
Plenum exposes functionality via MCP (Model Context Protocol).
┌──────────────────────────────────────────────────┐
│ MCP Client (e.g. Claude) │
└──────────────────────────────────────────────────┘
│
│ JSON-RPC over stdio
▼
┌──────────────────────────────────────────────────┐
│ plenum mcp (MCP Server) │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ PlenumServer (ServerHandler) │ │
│ │ │ │
│ │ #[tool] connect(...) │ │
│ │ #[tool] introspect(...) │ │
│ │ #[tool] query(...) │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ │ Calls library functions │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Plenum Library (lib.rs) │ │
│ │ • execute_connect() │ │
│ │ • execute_introspect() │ │
│ │ • execute_query() │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
Key principle: MCP tools are thin wrappers around library functions.
#[derive(Clone)]
pub struct PlenumServer;
impl ServerHandler for PlenumServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
name: "plenum".into(),
version: env!("CARGO_PKG_VERSION").into(),
}
}
}
#[tool(description = "Execute SQL query with capability constraints")]
async fn query(
&self,
Parameters(request): Parameters<QueryRequest>,
) -> Result<CallToolResult, McpError> {
// 1. Resolve connection from request
let config = resolve_config_from_request(&request)?;
// 2. Build capabilities from request
let caps = Capabilities {
allow_write: request.allow_write.unwrap_or(false),
allow_ddl: request.allow_ddl.unwrap_or(false),
max_rows: request.max_rows,
timeout_ms: request.timeout_ms,
};
// 3. Call library function (SAME as CLI)
let result = crate::execute_query(config, &request.sql, caps)?;
// 4. Wrap in MCP response
Ok(CallToolResult::success(result))
}Both interfaces call the same library functions:
CLI: MCP:
plenum query \ {
--name prod \ "tool": "query",
--sql "SELECT ..." \ "arguments": {
--allow-write "name": "prod",
"sql": "SELECT ...",
"allow_write": true
}
}
│ │
└───────────┬───────────────────────┘
│
▼
crate::execute_query()
│
▼
DatabaseEngine::execute()
Identical behavior guaranteed:
- Same capability validation
- Same SQL categorization
- Same connection resolution
- Same error handling
- Same JSON output format
Plenum's security model is based on capability constraints, not SQL validation.
-
Capability-based access control
- Read-only by default
- Explicit flags for write/DDL operations
- Pre-execution capability checks
-
Operation type restrictions
- SELECT vs INSERT/UPDATE/DELETE vs DDL
- Engine-specific categorization
-
Resource limits
- Row limits (
max_rows) - Query timeouts (
timeout_ms)
- Row limits (
-
Credential security
- No passwords in logs or error messages
- Support for environment variable references
- Credentials never cached in memory
-
SQL injection prevention
- Agent's responsibility to sanitize inputs
- Plenum passes SQL verbatim to database
-
Query semantic correctness
- No validation of table/column names
- No type checking
-
Business logic constraints
- No row-level security
- No data access policies
The calling agent MUST:
- Sanitize user inputs before constructing SQL
- Validate queries for safety
- Implement application-level security
Plenum assumes SQL passed to it is safe.
// ❌ WRONG: Agent constructs unsafe SQL
const userId = userInput; // Could be "1 OR 1=1"
exec(`plenum query --sql "SELECT * FROM users WHERE id = ${userId}"`);
// ✅ CORRECT: Agent sanitizes input
const userId = sanitizeInput(userInput);
const sql = `SELECT * FROM users WHERE id = ${userId}`;
exec(`plenum query --sql "${sql}"`);Storage:
- Credentials stored as plain JSON in config files
- User's responsibility to secure config files
- Support for environment variable references
Runtime:
- Passwords never logged
- Passwords never in error messages
- Passwords not cached between invocations
Best practices:
{
"connections": {
"prod": {
"engine": "postgres",
"host": "db.example.com",
"user": "app",
"password_env": "PROD_DB_PASSWORD", ← Use env var
"database": "production"
}
}
}Alternative considered: Connection pooling with persistent connections
Rejected because:
- Adds complexity (connection lifecycle management)
- Reduces determinism (pool state affects behavior)
- Increases risk of connection leaks
- Harder to reason about for agents
Stateless wins:
- Simpler implementation
- Predictable behavior
- No connection leaks possible
- Each invocation is independent
Alternative considered: Use sqlx for unified database interface
Rejected because:
- Introduces abstraction layer (violates core principle)
- Risk of cross-engine behavior leakage
- Vendor-specific quirks hidden behind abstraction
- Harder to debug engine-specific issues
Native drivers win:
- Maximum engine isolation
- Vendor-specific behavior preserved
- Clear separation of concerns
- Each engine owns its quirks
Alternatives considered:
- Full SQL parser (sqlparser-rs)
- Database EXPLAIN to categorize queries
- Trial execution with rollback
Rejected because:
- Parser: Too complex, unnecessary for categorization
- EXPLAIN: Requires database connection (fails for invalid credentials)
- Trial execution: Unsafe, side effects possible
Regex wins:
- Simple and explicit
- Engine-specific (respects SQL dialects)
- No external dependencies
- Fast and deterministic
Alternative considered: Human-friendly output with tables/colors
Rejected because:
- Agents don't need pretty printing
- Parsing structured data is harder than JSON
- Human UX features add complexity
JSON-only wins:
- Machine-parseable
- Deterministic serialization
- Easy for agents to consume
- Clear contract (JSON Schema)
Each command opens and closes a connection:
Command invocation time = Connection time + Execution time + Close time
Typical overhead:
- SQLite: < 10ms (file-based, no network)
- PostgreSQL: 50-200ms (network + auth)
- MySQL: 50-200ms (network + auth)
Trade-off accepted:
- Stateless design prioritized over performance
- Connection overhead is acceptable for agent use cases
- Agent can batch operations if needed
- Use SQLite for local/dev workflows (fastest)
- Batch queries in agent logic (not in Plenum)
- Use named connections (avoid repeated config lookup)
- Set max_rows to limit result set size
See benches/ directory for detailed performance benchmarks:
cargo bench --features sqliteTypical results (SQLite):
- Connection validation: ~5ms
- Simple introspection: ~20ms
- SELECT query (100 rows): ~15ms
- INSERT query: ~10ms
┌──────────────────┐
│ Integration │ ← Cross-engine consistency
│ Tests │ (tests/integration_tests.rs)
└──────────────────┘
│
┌────────────────────────┐
│ Output Validation │ ← JSON envelope schemas
│ Tests │ (tests/output_validation.rs)
└────────────────────────┘
│
┌──────────────────────────────┐
│ Edge Case Tests │ ← Large datasets, Unicode, etc.
│ │ (tests/edge_cases.rs)
└──────────────────────────────┘
│
┌────────────────────────────────────┐
│ Unit Tests │ ← Capability validation, etc.
│ (in each module) │ (src/**/*.rs)
└────────────────────────────────────┘
-
Unit tests (69 tests)
- Capability validation
- SQL categorization
- Config resolution
- Error handling
-
Integration tests (16 tests)
- Cross-engine query consistency
- Capability enforcement uniformity
- NULL handling across engines
-
Output validation tests (11 tests)
- JSON envelope structure
- Schema compliance
- No stdout pollution
-
Edge case tests (12 tests)
- Large datasets (5000+ rows)
- Unicode characters
- Binary data (BLOBs)
- Numeric extremes
- Timeouts
-
Snapshot tests
- JSON output stability
- Envelope format consistency
- Using
instacrate
Total: 108 tests passing
- Implement
rmcp-based server - Define MCP tool schemas with
schemars - Ensure CLI/MCP parity
- Review capability enforcement
- Audit credential handling
- Check error message safety
Potential additions (NOT in scope for MVP):
- Prepared statement support
- Transaction management
- Batch query execution
- Additional database engines (SQL Server, MongoDB)
- Observability (structured logging, metrics)
Guiding question for all additions:
"Does this make autonomous agents safer, more deterministic, or more constrained?"
If the answer is no, it does not belong in Plenum.
Plenum's architecture prioritizes:
- Agent safety through capability enforcement
- Determinism through stateless design
- Explicitness through no implicit behavior
- Simplicity through minimal abstractions
- Isolation through independent engine implementations
This architecture enables AI agents to interact with databases in a safe, predictable, and constrained manner.