Facio's Tool Surface Discipline: How AI Agents Get Exactly the Capabilities They Need Without Exposing the Entire System
AI agents gain power through tools. The agent calls a database query, an email send, a payment process, a deploy trigger. Each tool extends what the agent can do. The tools are the agent's hands in the world.
The naive approach exposes every tool to every agent. The agent has access to the entire tool catalog; the agent chooses based on intent. The risk: an agent that's supposed to handle customer support questions can also trigger a payment refund. An agent that's supposed to read documents can also deploy code. An agent that gets compromised has the keys to the kingdom.
Facio's tool surface discipline gives every agent a controlled capability set. The agent sees only the tools it needs; the tools it needs are scoped to the data it should access; the access is logged and auditable. The agent is powerful enough to do its work; the agent is constrained enough to not be a liability.
Here's how the discipline works, what it includes, and why tool surface management is what makes AI agents deployable in production without becoming security nightmares.
The Tool Surface Reality
Production AI agents face a tool surface problem:
Problem 1: Excessive capability. Agents that need to read documents also have the ability to delete them. Agents that need to look up customers can also create new ones. The blast radius of agent actions is too large.
Problem 2: Confused scope. An agent for the support team has access to the same tools as an agent for the finance team. The support agent doesn't need payment tools; the finance agent doesn't need support tools. Cross-scope access creates risk.
Problem 3: Compromised agents. If an agent is compromised (via prompt injection, credential theft, supply chain attack), the attacker has whatever capabilities the agent has. Minimizing agent capability limits the damage from compromise.
Problem 4: Compliance violations. Regulated workflows require separation of duties. The agent that approves a transaction shouldn't be the agent that initiates it. Without tool surface discipline, separation is impossible.
Problem 5: Accidental misuse. Agents are not always malicious; they can be wrong. An agent that thinks it should email a customer about a refund might email all customers. Tool surface discipline prevents the agent from even trying.
The naive approach — give every agent every tool — fails every test. The team ends up with a system where every action is a potential disaster.
The Tool Surface Discipline
Facio's tool surface discipline has five pillars. Each addresses a different aspect of capability control.
Pillar 1: Tool Allowlisting (Per-Agent Capability Sets)
Each agent has an explicit set of tools it's allowed to call:
# Tool allowlists per agent
agent_tool_allowlists = {
"customer_support_agent": [
"postgres.read_customer_data",
"postgres.read_order_data",
"slack.read_messages",
"slack.send_message",
"knowledge_base.search",
],
"finance_agent": [
"postgres.read_transaction_data",
"stripe.create_refund", # Scoped to refunds only
"stripe.read_payment_data",
"report_generator.create_financial_report",
],
"deployment_agent": [
"github.read_repository",
"github.create_pull_request",
"deployment.trigger_build", # Scoped to non-production
"logs.read_deployment_logs",
],
"executive_assistant_agent": [
"calendar.read_events",
"calendar.create_event",
"email.read_messages",
"email.send_message",
"slack.send_message",
],
}
def get_tools_for_agent(agent_type):
return agent_tool_allowlists.get(agent_type, [])
The allowlist is the agent's capability set. The agent cannot call tools not on its list. The blast radius is bounded.
Pillar 2: Argument Scoping (Per-Tool Data Constraints)
Even allowed tools have scoped arguments:
# Argument scoping per tool
tool_argument_scopes = {
"postgres.read_customer_data": {
"max_rows": 100,
"allowed_columns": ["id", "name", "email", "status"],
"blocked_columns": ["ssn", "date_of_birth", "payment_info"],
"filter_required": {"tenant_id": "agent_tenant"},
},
"stripe.create_refund": {
"max_amount_per_call": 1000, # $1000 cap
"max_amount_per_day_per_customer": 5000, # Daily cap
"allowed_reasons": ["duplicate", "fraudulent", "requested_by_customer"],
"blocked_reasons": ["other"],
"requires_customer_acknowledgment": True,
},
"deployment.trigger_build": {
"allowed_environments": ["staging", "dev"],
"blocked_environments": ["production"],
"requires_approval_for": ["production"],
"max_builds_per_day": 10,
},
"slack.send_message": {
"allowed_channels_pattern": "^(support|general|team-).*",
"blocked_channels_pattern": "^(executive|legal|finance-private).*",
"max_message_length": 4000,
"requires_dm_approval": True,
},
}
The argument scoping constrains what data the agent can access and what actions it can take within an allowed tool. The agent can't read PII even with a read tool.
Pillar 3: Dynamic Tool Loading (Context-Aware Capabilities)
Tools are loaded based on the current context, not statically available:
# Dynamic tool loading
def get_tools_for_context(agent_type, current_context):
base_tools = agent_tool_allowlists.get(agent_type, [])
# Add context-specific tools
if current_context.get("task") == "customer_inquiry":
base_tools.extend([
"order_lookup.read_order",
"knowledge_base.search_faq",
])
if current_context.get("escalation_level") == "high":
base_tools.append("human_agent.escalate")
if current_context.get("compliance_required"):
base_tools.append("audit_log.record_decision")
# Remove tools that don't apply to current context
if current_context.get("read_only_mode"):
base_tools = [t for t in base_tools if not is_write_tool(t)]
return base_tools
The dynamic loading ensures the agent has the right tools for the current context. The agent can't deploy code during a customer support conversation, even if deployment is on its base allowlist.
Pillar 4: Tool Invocation Logging (Audit Trail)
Every tool call is logged with full context:
# Tool invocation logging
tool_invocation_log = {
"invocation_id": "inv-abc-123-456",
"agent_id": "agent-support-007",
"agent_type": "customer_support_agent",
"tool_name": "postgres.read_customer_data",
"arguments": {"customer_id": "cust-12345", "tenant_id": "tenant-1"},
"result_summary": "returned 1 row",
"data_accessed": ["name", "email", "status"],
"data_blocked": ["ssn", "payment_info"],
"timestamp": "2026-07-25T10:23:45Z",
"user_id": "user-jane-doe",
"session_id": "session-abc-789",
"context": {"task": "customer_inquiry", "request_id": "req-456"},
"decision_provenance": "see trace-id-trace-123",
}
# Audit log query
audit_query = """
SELECT * FROM tool_invocations
WHERE agent_id = %s
AND timestamp >= %s
ORDER BY timestamp DESC
"""
The logging creates an audit trail. The team can answer: who called what, when, with what arguments, and why.
Pillar 5: Anomaly Detection (Suspicious Tool Use)
Tool use patterns are monitored for anomalies:
# Anomaly detection on tool use
tool_anomaly_detection = {
"unusual_tool_for_agent": {
"description": "Agent calls a tool outside its typical pattern",
"detection": "tool_name not in agent.historical_tool_usage",
"severity": "medium",
"action": "alert_and_log",
},
"argument_anomaly": {
"description": "Tool called with unusual arguments",
"detection": "arguments.significantly_different_from_historical_pattern",
"severity": "medium",
"action": "require_secondary_approval",
},
"frequency_anomaly": {
"description": "Tool called much more frequently than normal",
"detection": "tool_calls_per_minute > 2x historical_baseline",
"severity": "high",
"action": "rate_limit_and_alert",
},
"data_volume_anomaly": {
"description": "Tool returns much more data than usual",
"detection": "result_rows > 5x historical_baseline",
"severity": "high",
"action": "block_and_review",
},
"time_anomaly": {
"description": "Tool called at unusual time",
"detection": "tool_calls outside normal business hours for this agent",
"severity": "low",
"action": "log_only",
},
}
The anomaly detection catches suspicious patterns. The agent's compromise is detected early.
The Tool Surface Patterns
Several patterns emerge from disciplined tool surface management.
Pattern 1: Tool Composition Boundaries
Tools can be composed, but composition respects boundaries:
# Tool composition with boundaries
tool_composition_policies = {
"support_agent_can_read_then_respond": {
"tools": ["postgres.read_customer_data", "slack.send_message"],
"composition_allowed": True,
"constraint": "data_read_from_postgres must be referenced in slack_message",
},
"support_agent_cannot_initiate_then_refund": {
"tools": ["postgres.create_support_ticket", "stripe.create_refund"],
"composition_allowed": False,
"constraint": "agent cannot initiate AND refund in same workflow",
"exception": "human_approval_required",
},
"finance_agent_can_read_then_refund": {
"tools": ["postgres.read_transaction", "stripe.create_refund"],
"composition_allowed": True,
"constraint": "refund_amount <= transaction_amount",
},
"deployment_agent_can_read_then_deploy": {
"tools": ["github.read_repository", "deployment.trigger_build"],
"composition_allowed": True,
"constraint": "deployment.trigger_build must reference specific commit_sha",
},
}
Composition boundaries enforce separation of duties even when individual tools are allowed.
Pattern 2: Per-User Tool Delegation
Users can grant agents temporary tool access for specific tasks:
# Per-user tool delegation
user_tool_delegation = {
"delegation_grant": {
"user_id": "user-jane-doe",
"agent_id": "agent-support-007",
"tools_granted": ["stripe.create_refund"],
"scope": {"max_amount": 500, "valid_for": "1 hour", "max_uses": 5},
"expires_at": "2026-07-25T11:23:45Z",
"revocable": True,
"audit_log": True,
},
"delegation_enforcement": {
"on_tool_call": "verify_delegation_active AND scope_satisfied AND usage_under_limit",
"on_violation": "block_call AND notify_user_AND_security_team",
},
}
The per-user delegation gives users control over agent capabilities. The user grants access; the user can revoke.
Pattern 3: Tool Quota and Budgets
Each agent has tool usage budgets:
# Tool quotas per agent
tool_quotas = {
"customer_support_agent": {
"postgres.read_customer_data": {"max_per_hour": 100, "max_per_day": 1000},
"slack.send_message": {"max_per_hour": 50, "max_per_day": 500},
"knowledge_base.search": {"max_per_hour": 200, "max_per_day": 5000},
},
"finance_agent": {
"stripe.create_refund": {"max_per_hour": 10, "max_per_day": 50},
"stripe.read_payment_data": {"max_per_hour": 100, "max_per_day": 1000},
},
"deployment_agent": {
"deployment.trigger_build": {"max_per_hour": 5, "max_per_day": 20},
"github.create_pull_request": {"max_per_hour": 10, "max_per_day": 50},
},
}
The quotas prevent runaway agent behavior. The agent can't accidentally (or maliciously) call a tool thousands of times.
Pattern 4: Tool Health and Versioning
Tools have versions and the agent's tool set matches expectations:
# Tool versioning
tool_versioning = {
"postgres.read_customer_data": {
"version": "2.3.1",
"schema": {
"input": {"customer_id": "string", "tenant_id": "string"},
"output": {"id": "string", "name": "string", "email": "string", "status": "string"},
},
"breaking_change_policy": "major_version_bump_required",
"agent_tool_specs": {
"customer_support_agent": "2.x",
"finance_agent": "2.x",
"deployment_agent": "not_installed",
},
},
"stripe.create_refund": {
"version": "1.5.0",
"schema": {
"input": {"charge_id": "string", "amount": "integer", "reason": "string"},
"output": {"refund_id": "string", "status": "string"},
},
"agent_tool_specs": {
"finance_agent": "1.x",
},
},
}
The versioning ensures the agent calls tools with the expected schemas. Breaking changes are explicit. The agent doesn't call a tool with stale expectations.
Pattern 5: Tool Health Monitoring
Tool availability and performance are monitored:
# Tool health monitoring
tool_health = {
"postgres.read_customer_data": {
"availability_pct": 99.95,
"p50_latency_ms": 12,
"p99_latency_ms": 45,
"error_rate_pct": 0.02,
"last_incident": "2026-07-15T03:23:45Z",
},
"stripe.create_refund": {
"availability_pct": 99.99,
"p50_latency_ms": 230,
"p99_latency_ms": 1200,
"error_rate_pct": 0.05,
"last_incident": "none",
},
"deployment.trigger_build": {
"availability_pct": 99.5,
"p50_latency_ms": 1500,
"p99_latency_ms": 8000,
"error_rate_pct": 1.2,
"last_incident": "2026-07-22T14:12:33Z",
"degraded": True,
},
}
The health monitoring surfaces tool issues. The agent can route around unhealthy tools.
The Tool Surface Discipline Doesn't Do
Honest limitations:
- It doesn't make the agent trustworthy. The agent can still misuse the tools it has. The discipline bounds the damage, not the agent's intent.
- It requires maintenance. Allowlists need updates as the system evolves. Argument scopes need tuning.
- It can be too restrictive. Overly restrictive scopes prevent legitimate work. The discipline balances safety and capability.
- It doesn't prevent insider threats. A malicious operator can grant themselves access. The discipline protects against agent misuse, not operator misuse.
- It can be bypassed through composition. If the agent has many small tools, composition can recreate large capabilities. The discipline includes composition rules, but they're not perfect.
The Tool Surface as Operational Practice
Tool surface discipline is operational practice:
Allowlist review. The team reviews agent allowlists. They check for unnecessary access.
Argument scope tuning. The team tunes argument scopes based on observed needs. They loosen where too tight; they tighten where too loose.
Anomaly investigation. The team investigates anomaly alerts. They catch attacks and accidents.
Tool lifecycle management. The team manages tool versions, deprecations, and rollouts. The tool surface stays current.
The practice is what makes the discipline sustainable. Without it, the tool surface drifts. With it, the tool surface stays controlled.
The Compound Effect of Tool Surface Discipline
Tool surface discipline compounds:
- Reduced blast radius. Compromised agents have limited damage potential. The security is better.
- Better compliance. Separation of duties is enforceable. The compliance is achievable.
- Faster audits. Audit trails are complete and structured. The audits are quicker.
- Lower risk of accidents. Agents can't accidentally do things they shouldn't. The accidents are prevented.
- Clearer responsibility. Each agent has clear capabilities. The responsibility is defined.
The undisciplined approach has the opposite trajectory. Large blast radius, compliance gaps, slow audits, frequent accidents, unclear responsibility.
Bottom Line
AI agents gain power through tools. Without discipline, the power is unbounded. With discipline, the power is controlled.
Facio's tool surface discipline provides allowlisting, argument scoping, dynamic loading, invocation logging, and anomaly detection. The discipline makes AI agents powerful but safe.
The agent without tool surface discipline is a security liability. The agent with it is a controlled capability. The team trusts the controlled one. The compliance team accepts the controlled one.
Because AI agents in production call tools. The question is whether the tools are controlled or exposed. The tool surface discipline is what makes the answer controlled.
See the tool surface documentation for allowlist configuration, argument scoping, and anomaly detection tuning.