AID-H-017.002: Least-Privilege Tool Architecture

Version 1.20261015 · Actionable sub-technique

At the agent tool registry and dispatch boundary, expose only allowlisted, strongly typed, single-purpose capabilities with bounded parameters, projections, cardinality, credentials, and side effects. Deny generic shell, arbitrary SQL, unrestricted HTTP, broad filesystem, and equivalent ambient-power tools unless a separately isolated and authorized control explicitly admits them. Verify the effective tool schema and backend permissions for every deployed agent version.

Pillars: app

Lifecycle: building

Scope boundary

Owns architectural minimization of the effective agent tool surface through allowlisted, strongly typed, single-purpose capabilities with bounded parameters, projections, credentials, cardinality, destinations, and side effects.

AID-H-018.004: AID-H-017.002 designs the smallest safe tool primitives and removes ambient-power capabilities; AID-H-018.004 dynamically selects the allowed subset and budget for a particular authenticated request or session. A narrow registry is the architectural ceiling, while dynamic scope is the current runtime grant.

AID-H-018.001: AID-H-017.002 determines which granular tools should exist and what backend privilege they hold; AID-H-018.001 validates the parameters supplied to one exposed tool. A perfectly validated generic shell remains overprivileged by architecture.

Implementation guidance

AID-H-017.002-G001

Design granular, single-purpose tools instead of overly-permissive ones.
Concept:

Do not expose a generic execute_sql capability. Publish narrow methods with typed parameters, explicit result projection, bounded cardinality, and a database identity that cannot write or read unrelated tables.

Step 1: Implement narrow database tools
# File: agent_tools/customer_reads.py
from __future__ import annotations
import os

from dataclasses import dataclass

from psycopg import Connection
from psycopg.rows import dict_row

def _required_positive_int(name: str) -> int:
    value = int(os.environ[name])
    if value <= 0:
        raise RuntimeError(f"{name} must be a positive integer")
    return value

ORDER_QUERY_DEFAULT_LIMIT = _required_positive_int("VERIFIED_H017_ORDER_QUERY_DEFAULT_LIMIT")
ORDER_QUERY_MAX_LIMIT = _required_positive_int("VERIFIED_H017_ORDER_QUERY_MAX_LIMIT")



if ORDER_QUERY_DEFAULT_LIMIT > ORDER_QUERY_MAX_LIMIT:
    raise RuntimeError("verified default order limit exceeds maximum")


@dataclass(frozen=True)
class CustomerReadTools:
    connection: Connection

    def get_user_by_id(self, user_id: int) -> dict | None:
        if isinstance(user_id, bool) or not isinstance(user_id, int) or user_id <= 0:
            raise ValueError("user_id must be a positive integer")
        with self.connection.cursor(row_factory=dict_row) as cursor:
            cursor.execute(
                "SELECT user_id, status, plan FROM agent_safe.customer_directory WHERE user_id = %s",
                (user_id,),
            )
            return cursor.fetchone()

    def get_user_orders(self, user_id: int, limit: int = ORDER_QUERY_DEFAULT_LIMIT) -> list[dict]:
        if isinstance(user_id, bool) or not isinstance(user_id, int) or user_id <= 0:
            raise ValueError("user_id must be a positive integer")
        if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= ORDER_QUERY_MAX_LIMIT:
            raise ValueError("limit must be inside the verified order-query policy")
        with self.connection.cursor(row_factory=dict_row) as cursor:
            cursor.execute(
                "SELECT order_id, created_at, state FROM agent_safe.order_summary WHERE user_id = %s ORDER BY created_at DESC LIMIT %s",
                (user_id, limit),
            )
            return list(cursor.fetchall())
Step 2: Enforce a read-only database role
CREATE ROLE agent_customer_reader LOGIN;
ALTER ROLE agent_customer_reader SET default_transaction_read_only = on;
GRANT USAGE ON SCHEMA agent_safe TO agent_customer_reader;
GRANT SELECT ON agent_safe.customer_directory, agent_safe.order_summary TO agent_customer_reader;
REVOKE CREATE ON SCHEMA public FROM agent_customer_reader;

Privilege boundary: default_transaction_read_only is defense in depth and is session-overridable; the real write barrier is ownership plus explicit table/schema privileges, which must be read back and tested independently. Do not grant this role ownership, SUPERUSER, BYPASSRLS, or any write privilege.

Verification evidence: Export the published tool schema, database grants, and a negative test showing the role cannot insert, update, delete, access non-approved tables, or submit arbitrary SQL. Re-run the permission readback on every role or schema change and fail the release if a generic query parameter or write privilege appears.

Action: Register only the two typed methods above with the agent runtime; keep the connection and SQL text server-owned.

Keep the action safely bounded

Reconcile the complete population of every registered tool operation, route, query, and permission in the agent's assigned tool set; reject expired, revoked, or stale policy and asset bindings under the policy-defined freshness window.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-017.002: Least-Privilege Tool Architecture | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version