
# Ensure Gateway Internal Invocation Only Policy

:::note{title="AI Gateway Policy"}

This policy is for use with the [AI Gateway](/docs/ai-gateway/overview). See
the AI Gateway documentation to learn how to configure and govern AI models
with Zuplo.

:::

The Ensure Gateway Internal Invocation Only policy admits a request only when
this gateway made it with `context.invokeRoute`, and answers every request that
arrived over the network with `403 Forbidden`.

Add it to an application's `inboundPolicyChain` in place of AI Gateway
Authentication when the application is only ever called by other routes and
policies on the same gateway, such as the classifier application behind the
Smart Router or Prompt Injection Protection policies. Those calls never leave
the gateway, so an API key on them protects nothing; this policy checks the one
thing that matters. It has no options, makes no network calls, and never reads
the request body.

## Configuration

The configuration shows how to configure the policy in the 'policies.json' document.

```json title="config/policies.json"
{
  "name": "my-ai-gateway-internal-only-inbound-policy",
  "policyType": "ai-gateway-internal-only-inbound",
  "handler": {
    "export": "AIGatewayInternalOnlyInboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {}
  }
}
```

### Policy Configuration

- `name` <code className="text-green-600">&lt;string&gt;</code> - The name of your policy instance. This is used as a reference in your routes.
- `policyType` <code className="text-green-600">&lt;string&gt;</code> - The identifier of the policy. This is used by the Zuplo UI. Value should be `ai-gateway-internal-only-inbound`.
- `handler.export` <code className="text-green-600">&lt;string&gt;</code> - The name of the exported type. Value should be `AIGatewayInternalOnlyInboundPolicy`.
- `handler.module` <code className="text-green-600">&lt;string&gt;</code> - The module containing the policy. Value should be `$import(@zuplo/runtime)`.
- `handler.options` <code className="text-green-600">&lt;object&gt;</code> - The options for this policy. [See Policy Options](#policy-options) below.

### Policy Options

The options for this policy are specified below. All properties are optional unless specifically marked as required.

## Using the Policy

## How it works

`context.invokeRoute` runs a route on this gateway without leaving the process,
and the runtime marks the request it creates by setting `context.parentContext`
to the calling request's context. Nothing else sets that property. A request
that arrives over the network never has one, whatever headers, URL, or body it
carries. This policy passes a request when `context.parentContext` is present
and rejects it otherwise.

| Request origin                                                                 | Result |
| ------------------------------------------------------------------------------ | ------ |
| Any HTTP client, with or without an API key                                    | `403`  |
| `context.invokeRoute` from a policy, handler, or custom module on this gateway | Passes |
| The MCP server handler calling the route as a tool                             | Passes |
| An internal application calling another internal application                   | Passes |

The rejection is a `403 Forbidden` problem response. On AWS Bedrock Runtime
routes it uses the AWS error envelope (`AccessDeniedException`) so the AWS SDKs
surface the message.

The policy does not authenticate the caller and does not set `request.user`.
Metering records the request against the application with no consumer, and a
budget rule keyed on `request.user` has no value to accumulate for these
requests. Attach per-consumer budgets to the calling application instead.

## Add the policy

Declare the policy in `config/policies.json`:

```json
{
  "policies": [
    {
      "name": "ai-gateway-internal-only-inbound",
      "policyType": "ai-gateway-internal-only",
      "handler": {
        "export": "AIGatewayInternalOnlyInboundPolicy",
        "module": "$import(@zuplo/runtime)",
        "options": {}
      }
    }
  ]
}
```

Then select it in the application's `inboundPolicyChain` in place of
`ai-gateway-auth-v2-inbound`. Put it first so nothing else runs for a rejected
request:

```json
{
  "inboundPolicyChain": [
    { "name": "ai-gateway-internal-only-inbound" },
    { "name": "ai-gateway-model-filtering-v2-inbound" },
    { "name": "ai-gateway-metering-v2-inbound" }
  ]
}
```

The policy also works as a route-level inbound policy on any route that should
be reachable only through `context.invokeRoute`.

## Call the application

Call the application from a policy or handler on the same gateway. No
`Authorization` header is needed:

```ts
const response = await context.invokeRoute(
  `/${classifierAppId}/v1/chat/completions`,
  {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ model: "openai/gpt-5-mini", messages }),
  },
);
```

The Smart Router and Prompt Injection Protection policies already call their
classifier applications this way.

A direct call from outside the gateway is rejected:

```bash
curl https://gateway.example.com/my-classifier-app/v1/chat/completions \
  --header "Content-Type: application/json" \
  --data '{
    "model": "openai/gpt-5-mini",
    "messages": [{ "role": "user", "content": "Hello" }]
  }'
```

The response is `403 Forbidden`, and its `detail` explains that the application
only accepts requests made from inside the gateway with `context.invokeRoute`.

## What it does not check

This policy asserts that a request originated inside this gateway deployment,
not that the caller is trusted. Any route on the same gateway that forwards
requests through `context.invokeRoute`, including an MCP server that exposes the
application as a tool, reaches the application. Protect such routes with their
own authentication, exactly as you would an internal URL.

Read more about [how policies work](/articles/policies)
