Skip to lesson

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.

The wrapper owns the query template and the model supplies delivery_id, while authenticated_customer_id comes from trusted request context, not from the modelTwo sources sit outside the wrapper. The model supplies delivery_id; trusted request context supplies authenticated_customer_id. Inside the wrapper, its query template and both values meet where values are bound through placeholders: the wrapper owns the query template, and the model supplies values.The wrappersupplies valuesquery templatebind values throughplaceholdersthe modeldelivery_idtrusted requestcontextauthenticated_customer_id

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:

TargetPreferred interfaceEnforcement mechanismCost and failure concern
REST APIFixed business operation with typed argumentsConfigured destination, user-scoped credentials, downstream object authorizationNetwork latency and uncertain outcomes after a lost response
DatabaseFixed query with bound valuesSQL placeholders, scoped predicates, database privileges, explicit transaction controlConnection occupancy, locks, and commit failures
Local functionSpecific typed computation or actionNarrow callable, explicit input model, minimum execution permissionsCPU 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

Note:

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

References