A dispatcher turns an accepted request into a possible action. That makes its order of operations important. A registry supplies a small set of trusted callables; the request’s name is only a dictionary key. Do not evaluate it as Python, import a module from it or discover arbitrary attributes. A tool request should not expand the application’s capability surface.
Registration and permission answer different questions. A registered delete operation may be unavailable in a read-only session. The dispatcher therefore checks an independent allowed set before looking for an executable result. The notes assistant later exposes only search and read operations over explicitly supplied records.
Return a structured error for an unknown name, a denied known tool or invalid arguments. Do not filter away unwanted argument fields: that changes a rejected request into a different accepted one. The application can describe a correction to the model through its result, but should keep the evidence of the original attempt intact.
The registry can also attach a trusted policy callback for semantic or object-level decisions. It receives a copy of arguments, returns an error code or None, and runs before the handler. A policy failure should not expose an internal exception message to the model.
registry = {'read_note': {'fields': {'id': 'string'}}}
allowed = set()
print('read_note' in registry)
print('read_note' in allowed)A useful test proves that rejection prevents effects. Returning denied is insufficient if a handler already read a private record or changed state. Our fixtures use a list as a visible effect counter, then assert that it remains empty for denied and malformed calls. This tests the ordering directly.
Invocation and result normalization are supplied helpers in this lesson. Read them, but focus the build on gates before the call. The next lesson opens that helper and defines how successful values, expected failures, unexpected failures and output-size limits become model-visible data.
effects = []
def read_record():
effects.append('read')
if 'read_note' in set():
read_record()
print(effects)Build the dispatcher and test one accepted call, an unknown name, a registered-but-disallowed tool, extra arguments and a denying object policy. Count handler invocations in every case. These gates are an application policy model inside the existing sandbox; they do not replace OS isolation for arbitrary learner programs or future real tool executors.