NVIDIANVIDIA-Certified Professional: Agentic AI2026-102.3 Build and connect custom tools, APIs, and functions for external system interaction.
Constrain external actions inside validated adapters
Completed by the knowledge check
Learning objective
Wrap REST APIs, databases, and functions as validated tools with explicit side-effect boundaries.
Core idea
A tool adapter translates a model’s proposed arguments into an operation on a real system. That translation is an authority boundary: the adapter chooses the endpoint, credentials, query, and permitted effects. Exposing a convenient generic connector can give the model much more power than the business task needs.
See 2.3.1 for tool names and schemas. This lesson adds the checks between accepting arguments and making an external change. OWASP calls the combination of excessive functionality, permissions, and autonomy Excessive Agency. Its remedies include narrow extensions, minimum permissions, execution in the user’s context, and approval for high-impact actions. Implement those decisions in the adapter and downstream service rather than relying on the model to obey a description.
How it works
Build one adapter around one business capability
In NeMo Agent Toolkit, register a custom function with @register_function(config_type=...) and yield FunctionInfo.from_fn() around the callable. Supply an explicit input_schema where annotations alone do not express the required validation. This lets the toolkit validate inputs before the wrapped operation proceeds.
The registration coroutine also owns resource setup and cleanup. NVIDIA documents opening a resource in a context manager around the yielded function, then closing it when that context exits. Put an HTTP client or database connection lifecycle there rather than creating unmanaged resources on every invocation. For user-bound state, use @register_per_user_function or @register_per_user_function_group: instances are created lazily on each user’s first request. Per-user functions require explicit input and output schemas, and their workflow must also be registered as per-user. This isolates state but allocates an instance and its resources per user instead of one shared client; size connections and memory accordingly.
Use a typed output model to project the external response into the small set of fields the agent needs. A valid request followed by malformed upstream data is an adapter failure, not a successful answer. Keep the distinction visible to the caller instead of substituting invented values.
Wrap REST access without exposing arbitrary destinations
An application-defined lookup_delivery tool can accept delivery_id while its configured adapter chooses the delivery service and lookup operation. Do not replace that capability with a general fetch_url tool whose destination the model controls. OWASP specifically recommends granular functionality instead of open-ended URL-fetching and shell extensions.
Keep credentials out of model-provided arguments. Execute in the authenticated user’s permitted scope, or use a narrowly privileged service identity where that is the intended design. The downstream service must check the requested object’s ownership on each operation; validating that delivery_id is a string does not establish access.
Separate a lookup from a mutation such as reschedule_delivery. The latter should have its own allowed fields and server-side authorization checks. If the action needs approval, the adapter’s mutation handler verifies the approval record for that exact requested action before it sends the request. A model-generated approved: true argument is a claim by the model, not approval from an authorized person. See 9.4 and 10.4 for approval and escalation design.
These are application capability names and checks, not built-in NeMo Agent Toolkit components. The toolkit supplies the callable interface; the integration team supplies the downstream policy and operation.
Wrap database access around fixed queries
Prefer get_delivery_window(delivery_id) to an unrestricted execute_sql(query) interface for a delivery-status assistant. The wrapper owns the query template; the model supplies values. With Python’s sqlite3, bind values through placeholders rather than interpolating them into SQL. The following synchronous SQLite fragment shows the query shape, not its scheduling inside an async adapter:
row = connection.execute(
"SELECT delivery_id, window_start FROM deliveries "
"WHERE delivery_id = ? AND customer_id = ?",
(delivery_id, authenticated_customer_id),
).fetchone()Here authenticated_customer_id comes from trusted request context, not from the model. The predicate scopes the lookup, while the service’s database permissions determine which tables and operations its identity can access. Parameter binding prevents supplied values from becoming SQL syntax; it does not grant tenant isolation by itself.
The wrapper owns the query template and the model supplies delivery_id, while authenticated_customer_id comes from trusted request context, not from the model.
A synchronous database call inside an async tool blocks the event loop and delays other calls. asyncio.to_thread() can move blocking I/O off that loop, but SQLite’s default check_same_thread=True rejects using a connection from another thread. Create and use the connection within the same worker-thread operation, or choose the production database’s async driver; do not move calls on a registration-time SQLite connection between threads.
For mutations, define transaction behavior explicitly. Python documents Connection.commit() and Connection.rollback() for committing or rolling back pending work; their effect depends on transaction configuration, including autocommit. Do not describe every database call as durable merely because it returned a row. Choose transaction mode deliberately and return the business success result after the intended commit completes.
Treat local functions according to their effects
A Python function is not necessarily harmless because it makes no network request. A callable that writes a file, launches a process, or changes shared state has effects too. Conversely, a pure calculation can be exposed with a typed input and result without granting external credentials.
Expose a specific calculation such as estimate_delivery_cost, rather than a shell or arbitrary-code tool for the same purpose. Keep calculation and booking separate. This implements OWASP’s recommendation to minimize extension functionality and avoids turning a bounded computation into unrestricted execution.
An execution failure after a mutation can leave the result uncertain. Do not equate “the agent did not receive confirmation” with “nothing changed.” See 1.8.2 for failure isolation and 2.4 for retry and recovery decisions. Here the adapter’s responsibility is to preserve a truthful result distinction and avoid reporting an unconfirmed write as success.
Key distinctions and trade-offs
Choose the wrapper according to the external capability and its effects:
| Target | Preferred interface | Enforcement mechanism | Cost and failure concern |
|---|---|---|---|
| REST API | Fixed business operation with typed arguments | Configured destination, user-scoped credentials, downstream object authorization | Network latency and uncertain outcomes after a lost response |
| Database | Fixed query with bound values | SQL placeholders, scoped predicates, database privileges, explicit transaction control | Connection occupancy, locks, and commit failures |
| Local function | Specific typed computation or action | Narrow callable, explicit input model, minimum execution permissions | CPU work or local effects hidden behind an innocent name |
A read-only tool description helps the model choose, while a read-only database identity constrains what the implementation can do. Both matter, but only the latter enforces database permissions. Similarly, validating a response shape makes it consumable; it does not prove the external system performed the requested action correctly.
Architecture / decision scenario
A grocery retailer’s delivery assistant supports 300 status lookups per minute and 20 rescheduling requests per minute. Lookups have a 1-second service target; a reschedule changes a paid delivery slot and requires customer confirmation. The team proposes one generic API-and-SQL tool to reduce integration work.
The integration owner rejects that interface because it mixes 300 reads with 20 potential writes under one credential set. The REST adapter exposes lookup_delivery and a separate reschedule_delivery; the latter checks the confirmed slot and authenticated customer before calling the scheduling service. The database adapter uses a fixed, parameterized delivery-window query with a customer predicate and read permissions. To preserve the one-second target under concurrent lookups, its synchronous SQLite work creates, uses, and closes the connection inside one asyncio.to_thread() operation. The local estimate_delivery_cost function computes a quote without booking anything.
The tempting alternative is to let the model decide whether a request is “safe” and then execute any supplied endpoint or SQL. That bypasses the downstream scope checks and makes quote generation capable of booking. The owner accepts the maintenance cost of three narrow adapters because the business requires different authority for lookup, calculation, and mutation. A quote can satisfy the response target without waiting for a write; a reschedule receives success only after the scheduling service confirms it.
Common exam traps
- Treating input validation as downstream authorization.
- Giving a lookup adapter database write permissions because its description says it only reads.
- Binding SQL values while letting the model choose arbitrary query text.
- Accepting a model-supplied user ID or approval flag as trusted context.
- Treating every local callable as a pure computation.
- Reporting a mutation as unsuccessful solely because its response was lost.
Where the exam guide differs
The study guide’s suggested readings use “NVIDIA Agent Intelligence Toolkit (1.1.0).” The cited product documentation uses NeMo Agent Toolkit. Interpret the guide’s name as the product lineage; implement wrappers through the documented custom-function interface, with application-owned authorization and side-effect checks inside the callable or downstream service.
What to remember
Wrap a business operation, not an unrestricted system interface. Validate arguments and results, bind database values, keep identity in trusted context, and enforce permissions downstream. Separate computation, lookup, and mutation so each exposes only the authority its task requires.
Knowledge check
A delivery-status assistant must let each customer see only their own deliveries. The adapter uses a shared database identity that can read every customer’s rows. A teammate proposes an execute_sql(query) tool so the model can answer any status question. Which database adapter fits the requirement?
execute_sql and connect it through a read-only database roledelivery_id and a model-supplied customer_idexecute_sql and escape the model’s query text before it runsdelivery_id and the session’s customer IDdelivery_id , and the customer predicate comes from trusted request context, which is what scopes the lookup to the caller. A read-only role prevents writes but still lets the model read any customer’s rows, and a model-supplied customer_id is a claim rather than identity.reschedule_delivery changes a paid delivery slot and requires the customer’s confirmation. In testing, the model calls it with "approved": true and a valid schema. What must the adapter’s handler check before it calls the scheduling service?
approved argument passed schema validation as a booleanAn async lookup tool opens one SQLite connection in its registration coroutine and calls it directly. Under concurrent lookups, other tool calls stall. Wrapping each connection.execute() in asyncio.to_thread() then raises ProgrammingError. Which change keeps the event loop free?
asyncio task on the same event loopto_thread()to_thread() callcheck_same_thread=True rejects a connection used from a thread other than the one that created it. Doing all the connection work inside one worker-thread operation satisfies both; another task on the same loop still blocks it, and a connection opened on the loop thread still crosses threads.A support assistant moves its ticketing adapter to @register_per_user_function, so each user’s ticket-system session stays isolated. The deployment serves several thousand distinct users a day, and each instance opens its own HTTP client. What must the team plan for?
References
- NVIDIA, Writing Custom Functions
- NVIDIA, Functions
- Python Software Foundation, sqlite3 — DB-API 2.0 interface for SQLite databases
- NVIDIA, Writing Per-User Functions
- Python Software Foundation, Coroutines and Tasks
- OWASP, LLM06 Excessive Agency
- NVIDIA, NVIDIA-Certified Professional: Agentic AI Exam Study Guide