AFP engineering series · Chapter 8 of 25
Design narrow tools whose backend permissions remain correct even when the model proposes unauthorized arguments.
The tool is a security boundary
A model-generated function call is untrusted input to an application service. Its JSON shape may be valid while its meaning is unauthorized. An order identifier can exist but belong to another tenant. An amount can be positive but exceed the remaining refundable balance. A destination can be syntactically valid but have no relationship to the original purchase.
For Northstar, expose read_order, prepare_refund, and execute_approved_refund as separate capabilities. Avoid a generic tool that accepts arbitrary SQL or an unrestricted administrative command. Narrow capabilities reduce the number of policy decisions the model is implicitly allowed to make.
Separate actor, subject, and resource
The actor is the service credential making the downstream request. The subject is the authenticated person or workload on whose behalf it acts. The resource is the order, account, or document being accessed. Logs and authorization checks need all three. A service account with broad database access does not imply that every user of the assistant has the same access.
Resolve tenant scope from the authenticated request and bind it to the invocation. If the model supplies a tenant_id argument, either reject it or treat it as a requested resource attribute that must match the trusted scope. Never let it replace the caller's tenant. Recheck access after resolving the resource, because an identifier's format does not establish ownership.
Make capabilities smaller than roles
An administrator role may allow many operations in a conventional console. An assistant handling one refund does not need all of them. Derive a task-specific capability with a resource set, allowed operation, value limit, expiry, and intended audience. Keep its issuance outside the model and avoid placing reusable credentials into the prompt.
Delegation should attenuate authority. A specialist subagent receives only the capabilities needed for its subtask, not the parent's entire credential set. If a downstream service cannot enforce the narrower scope, use an adapter that can, or do not expose that action through the assistant. Describing a restriction in natural language is not equivalent to enforcing it.
Implement checks in a stable order
Illustrative python; adapt the persistence and identity adapters to your service.
def authorize_refund(principal, order, proposal, capability):
if principal.tenant != order.tenant:
return "deny_tenant"
if order.id not in capability.order_ids:
return "deny_resource"
if capability.operation != "prepare_refund":
return "deny_operation"
if type(proposal.amount_minor) is not int or proposal.amount_minor <= 0:
return "invalid_amount"
if proposal.amount_minor > capability.limit_minor:
return "deny_limit"
return "eligible_for_next_check" # Not approval to pay.This pure policy fragment deliberately stops before execution. Production code must also validate expiry, audience, currency, current order state, and approval binding, then reserve balance transactionally. Use precise numeric types and reject coercions such as booleans accepted as integers. Avoid returning resource-existence details to callers who lack access.
Order checks so inexpensive authentication and schema validation happen before costly lookups, while preserving equivalent denial behavior where information disclosure matters. Cache only decisions whose invalidation and expiry semantics are understood. A cached allow cannot outlive a revoked entitlement simply because the assistant's session is still open.
Constrain the transport and the destination
A narrow business tool can still call an unsafe URL if its implementation accepts a model-selected endpoint. Resolve service destinations from configuration and approved resource identifiers. Restrict network egress and validate redirects where external fetching is required. Do not forward bearer tokens to an arbitrary host named in a tool result.
Use separate credentials for read and write services, and keep secrets in the runtime's credential mechanism rather than session memory. Log credential identity or scope identifiers, not the secret values. Rotating a credential should not require editing the assistant's prompt or retraining the model.
Test bypass paths and concurrent execution
Call the backend directly without ADK and verify that unauthorized requests still fail. Test a valid user with another tenant's order, a valid order with an expired capability, an allowed operation with a larger amount, and a changed destination after approval. Then run two concurrent requests against the same remaining balance.
The concurrency test should assert the final ledger invariant, not only individual HTTP statuses. Two requests may each be valid at the moment they read the balance. Without a lock, compare-and-swap, or transactional reservation, both can commit and exceed the limit. Framework callbacks cannot fix a race inside the service that owns the money.
Worked capability design: delegation to a refund specialist
The root support agent can read an order and ask a refund specialist to prepare a proposal. A tempting implementation forwards the root agent's broad service credential to the specialist. That makes delegation an authority expansion surface: any mistake in the specialist can use permissions unrelated to the subtask.
Instead, the application creates a short-lived capability for one order and one operation, prepare_refund. It includes tenant scope, a maximum amount, an expiry, and the intended service audience. The capability is stored or transmitted through the trusted runtime, not pasted into the prompt. The specialist receives only an opaque reference that the service can resolve under the authenticated invocation.
The specialist proposes an amount within the cap. The preparation endpoint verifies the capability and current order ownership, then creates a proposal. It does not execute the refund. A separate approval and execution path checks the current balance and decision record. This separation means a compromised specialist cannot convert preparation authority into payment authority simply by choosing another tool name.
Test attenuation by asking the specialist to access a second order, increase the cap, change the tenant, or reuse the capability after expiry. Each should fail at the service boundary. Test a legitimate proposal as well. If the capability is delegated again, its scope should stay the same or become narrower; a child cannot issue a broader credential for itself.
Consider revocation during execution. The order may be reassigned or the parent request cancelled after the capability is issued. Decide which operations require an online revocation check and which can rely on a short expiry, based on their consequence. For payment execution, current eligibility and authorization remain mandatory regardless of the earlier preparation capability.
The resulting audit trail links root request, delegated task, capability identifier, proposal, and final action. It avoids exposing the credential itself. This makes delegation explainable and bounded: the specialist can help solve the task, but the authority it exercises is explicit, limited, and independently checked.
Expose useful results without expanding authority
Return the smallest result the agent needs: an order status, an eligibility explanation, or an operation identifier. Do not return full customer profiles or internal credentials merely because the service has them. Tool results are another source of untrusted or sensitive context and should retain classification and provenance.
ADK's safety guidance distinguishes agent and user authorization and discusses in-tool controls. The capability design here builds on those concepts but requires application-specific implementation. A successful rollout demonstrates that the same permission rules hold through ADK, a direct API client, retries, and delegated workflows. The framework should make enforcement convenient, not be the only place it exists.
Practice and continue
Open the interactive companion lab to inspect the assumptions and outcomes with synthetic data. Then return to the 25-chapter AFP learning path.
Sources and implementation scope
Primary references checked October 15, 2026. Framework details follow the linked documentation; the Northstar cases, policy contracts, and test designs are original illustrative designs. Python snippets labeled illustrative are not complete production integrations. ADK package metadata checked for this series reported 2.12.0; pin and test the version used by your application.