zuplo 7.9.14 → 7.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/ai-gateway/bedrock-runtime.mdx +1 -1
- package/docs/ai-gateway/custom-policies.mdx +1 -1
- package/docs/ai-gateway/fallback.mdx +1 -1
- package/docs/ai-gateway/integrations/ai-sdk.mdx +5 -5
- package/docs/ai-gateway/integrations/claude-code.mdx +6 -8
- package/docs/ai-gateway/integrations/codex.mdx +6 -5
- package/docs/ai-gateway/integrations/goose.mdx +3 -3
- package/docs/ai-gateway/integrations/langchain.mdx +1 -1
- package/docs/ai-gateway/integrations/openai.mdx +3 -3
- package/docs/ai-gateway/managing-providers.mdx +1 -1
- package/docs/ai-gateway/openrouter.mdx +14 -13
- package/docs/ai-gateway/overview.mdx +4 -2
- package/docs/ai-gateway/providers.mdx +2 -2
- package/docs/ai-gateway/source-control.mdx +61 -2
- package/docs/ai-gateway/universal-api.mdx +1 -1
- package/docs/ai-gateway/user-apps.mdx +5 -2
- package/docs/articles/accounts/audit-logs.mdx +3 -9
- package/docs/articles/environment-variables.mdx +56 -231
- package/docs/articles/graphql-support.mdx +59 -18
- package/docs/articles/local-development-env-variables.mdx +37 -8
- package/docs/articles/logging.mdx +21 -0
- package/docs/articles/opentelemetry.mdx +1 -1
- package/docs/cli/source-upgrade.mdx +39 -0
- package/docs/cli/source-upgrade.partial.mdx +11 -0
- package/docs/concepts/ai-gateway.mdx +1 -1
- package/docs/dev-portal/zudoku/guides/environment-variables.md +75 -0
- package/docs/dev-portal/zudoku-partials/guides/environment-variables.partial.md +74 -0
- package/docs/handlers/mcp-server.mdx +72 -0
- package/docs/mcp-server/overview.mdx +7 -0
- package/docs/mcp-server/tools.mdx +72 -0
- package/docs/policies/_index.md +1 -1
- package/docs/policies/ai-gateway-auth-inbound/doc.md +2 -2
- package/docs/policies/ai-gateway-configuration-executor-inbound/doc.md +3 -3
- package/docs/policies/ai-gateway-prompt-injection/doc.md +1 -2
- package/docs/policies/ai-gateway-prompt-injection/schema.json +2 -2
- package/docs/policies/ai-gateway-smart-router-inbound/doc.md +112 -29
- package/docs/policies/ai-gateway-smart-router-inbound/schema.json +109 -12
- package/docs/policies/internal-only-inbound/doc.md +206 -0
- package/docs/policies/internal-only-inbound/intro.md +10 -0
- package/docs/policies/internal-only-inbound/schema.json +75 -0
- package/docs/programmable-api/environment.mdx +220 -48
- package/docs/programmable-api/hooks.mdx +24 -0
- package/docs/programmable-api/mcp-sdk.mdx +22 -0
- package/package.json +5 -5
- package/docs/policies/ai-gateway-internal-only-inbound/doc.md +0 -102
- package/docs/policies/ai-gateway-internal-only-inbound/intro.md +0 -11
- package/docs/policies/ai-gateway-internal-only-inbound/schema.json +0 -50
|
@@ -61,8 +61,24 @@
|
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
"classifier": {
|
|
64
|
-
"
|
|
65
|
-
"
|
|
64
|
+
"app": {
|
|
65
|
+
"appId": "$env(CLASSIFIER_APP_ID)",
|
|
66
|
+
"adapter": "systemOne",
|
|
67
|
+
"model": "jev/jev-latest"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"modelsByComplexity": {
|
|
71
|
+
"low": "openai/gpt-4o-mini",
|
|
72
|
+
"medium": "openai/gpt-4o",
|
|
73
|
+
"high": "openai/gpt-5"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"classifier": {
|
|
78
|
+
"app": {
|
|
79
|
+
"appId": "$env(CLASSIFIER_APP_ID)",
|
|
80
|
+
"adapter": "systemOne",
|
|
81
|
+
"model": "jev/jev-latest"
|
|
66
82
|
}
|
|
67
83
|
},
|
|
68
84
|
"smartRoutingEnabled": true,
|
|
@@ -71,13 +87,27 @@
|
|
|
71
87
|
"medium": "openai/gpt-4o",
|
|
72
88
|
"high": "openai/gpt-5"
|
|
73
89
|
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"classifier": {
|
|
93
|
+
"app": {
|
|
94
|
+
"appId": "$env(CLASSIFIER_APP_ID)",
|
|
95
|
+
"adapter": "chatCompletions",
|
|
96
|
+
"model": "openai/gpt-4o-mini"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"modelsByComplexity": {
|
|
100
|
+
"low": "openai/gpt-4o-mini",
|
|
101
|
+
"medium": "openai/gpt-4o",
|
|
102
|
+
"high": "openai/gpt-5"
|
|
103
|
+
}
|
|
74
104
|
}
|
|
75
105
|
],
|
|
76
106
|
"properties": {
|
|
77
107
|
"classifier": {
|
|
78
108
|
"type": "object",
|
|
79
109
|
"title": "Classifier",
|
|
80
|
-
"description": "Where the
|
|
110
|
+
"description": "Where the prompt is classified. Set exactly one of `app`, `chatCompletions`, or `jev`. `app.adapter` selects the API that classifier app exposes. Setting more than one section is a configuration error.",
|
|
81
111
|
"additionalProperties": false,
|
|
82
112
|
"minProperties": 1,
|
|
83
113
|
"maxProperties": 1,
|
|
@@ -98,23 +128,30 @@
|
|
|
98
128
|
"model": {
|
|
99
129
|
"type": "string",
|
|
100
130
|
"title": "Classifier Model",
|
|
101
|
-
"description": "The model (`providerName/model`) the classifier app uses to evaluate the user's request. E.g. `openai/gpt-4o-mini`.",
|
|
131
|
+
"description": "The model (`providerName/model`) the classifier app uses to evaluate the user's request. `providerName` is the name of the provider as configured on the classifier app, not a fixed vendor id. E.g. `openai/gpt-4o-mini`, or `jev/jev-latest` when the Jev provider is named `jev`.",
|
|
102
132
|
"pattern": "^[^/\\s]+/.+$",
|
|
103
133
|
"examples": ["openai/gpt-4o-mini"]
|
|
104
134
|
},
|
|
105
135
|
"apiKey": {
|
|
106
136
|
"type": "string",
|
|
107
137
|
"title": "Classifier App API Key",
|
|
108
|
-
"description": "API key sent as `Authorization: Bearer` when invoking the classifier app. Omit it when the classifier app runs the
|
|
138
|
+
"description": "API key sent as `Authorization: Bearer` when invoking the classifier app. Omit it when the classifier app runs the Internal Only policy: the classifier call never leaves the gateway, so that policy accepts it and rejects everything arriving over the network, and no credential is sent. Set it only when the classifier app authenticates callers with API keys.",
|
|
109
139
|
"examples": ["$env(CLASSIFIER_APP_API_KEY)"],
|
|
110
140
|
"x-advanced": true
|
|
141
|
+
},
|
|
142
|
+
"adapter": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"enum": ["chatCompletions", "systemOne"],
|
|
145
|
+
"default": "chatCompletions",
|
|
146
|
+
"title": "Classifier App Adapter",
|
|
147
|
+
"description": "API contract exposed by the classifier app. chatCompletions calls /v1/chat/completions; systemOne calls /v1/systemone for models such as Jev. The app owns provider credentials, pricing, metering and analytics."
|
|
111
148
|
}
|
|
112
149
|
}
|
|
113
150
|
},
|
|
114
151
|
"chatCompletions": {
|
|
115
152
|
"type": "object",
|
|
116
153
|
"title": "Chat Completions API",
|
|
117
|
-
"description": "Classify by calling an OpenAI-compatible Chat Completions API directly, outside the gateway. The model must support structured outputs (`response_format` with a JSON schema).",
|
|
154
|
+
"description": "Deprecated: use `classifier.app` instead. Classify by calling an OpenAI-compatible Chat Completions API directly, outside the gateway. The model must support structured outputs (`response_format` with a JSON schema).",
|
|
118
155
|
"additionalProperties": false,
|
|
119
156
|
"required": ["apiKey", "model"],
|
|
120
157
|
"properties": {
|
|
@@ -137,12 +174,14 @@
|
|
|
137
174
|
"default": "https://api.openai.com/v1",
|
|
138
175
|
"x-advanced": true
|
|
139
176
|
}
|
|
140
|
-
}
|
|
177
|
+
},
|
|
178
|
+
"deprecated": true,
|
|
179
|
+
"doNotSuggest": true
|
|
141
180
|
},
|
|
142
181
|
"jev": {
|
|
143
182
|
"type": "object",
|
|
144
183
|
"title": "Jev (TypeSafe)",
|
|
145
|
-
"description": "Classify with Jev, TypeSafe's classification model, by calling a System One API directly: TypeSafe's by default, or another host such as OpenRouter through `baseUrl`. Jev answers typed questions instead of generating text, so it takes no `classifierPrompt`.",
|
|
184
|
+
"description": "Deprecated: use `classifier.app` with `adapter` `systemOne` instead. Classify with Jev, TypeSafe's classification model, by calling a System One API directly: TypeSafe's by default, or another host such as OpenRouter through `baseUrl`. Jev answers typed questions instead of generating text, so it takes no `classifierPrompt`.",
|
|
146
185
|
"additionalProperties": false,
|
|
147
186
|
"required": ["apiKey"],
|
|
148
187
|
"properties": {
|
|
@@ -166,7 +205,9 @@
|
|
|
166
205
|
"default": "https://api.typesafe.ai",
|
|
167
206
|
"x-advanced": true
|
|
168
207
|
}
|
|
169
|
-
}
|
|
208
|
+
},
|
|
209
|
+
"deprecated": true,
|
|
210
|
+
"doNotSuggest": true
|
|
170
211
|
}
|
|
171
212
|
}
|
|
172
213
|
},
|
|
@@ -238,7 +279,7 @@
|
|
|
238
279
|
},
|
|
239
280
|
"classifierPrompt": {
|
|
240
281
|
"title": "Classifier Prompt",
|
|
241
|
-
"description": "Prompt
|
|
282
|
+
"description": "Prompt for classifier.app (its default or chatCompletions adapter) and classifier.chatCompletions. Omit to use the built-in prompt. classifier.app.adapter systemOne and classifier.jev take no prompt; combining them with classifierPrompt is a configuration error.",
|
|
242
283
|
"oneOf": [
|
|
243
284
|
{
|
|
244
285
|
"type": "string",
|
|
@@ -308,6 +349,24 @@
|
|
|
308
349
|
"x-show-example": false,
|
|
309
350
|
"x-advanced": true
|
|
310
351
|
}
|
|
352
|
+
},
|
|
353
|
+
"not": {
|
|
354
|
+
"required": ["classifierPrompt", "classifier"],
|
|
355
|
+
"properties": {
|
|
356
|
+
"classifier": {
|
|
357
|
+
"required": ["app"],
|
|
358
|
+
"properties": {
|
|
359
|
+
"app": {
|
|
360
|
+
"required": ["adapter"],
|
|
361
|
+
"properties": {
|
|
362
|
+
"adapter": {
|
|
363
|
+
"const": "systemOne"
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
311
370
|
}
|
|
312
371
|
}
|
|
313
372
|
},
|
|
@@ -335,8 +394,28 @@
|
|
|
335
394
|
"module": "$import(@zuplo/runtime)",
|
|
336
395
|
"options": {
|
|
337
396
|
"classifier": {
|
|
338
|
-
"
|
|
339
|
-
"
|
|
397
|
+
"app": {
|
|
398
|
+
"appId": "$env(CLASSIFIER_APP_ID)",
|
|
399
|
+
"adapter": "systemOne",
|
|
400
|
+
"model": "jev/jev-latest"
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
"modelsByComplexity": {
|
|
404
|
+
"low": "openai/gpt-4o-mini",
|
|
405
|
+
"medium": "openai/gpt-4o",
|
|
406
|
+
"high": "openai/gpt-5"
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"export": "AIGatewaySmartRouterInboundPolicy",
|
|
412
|
+
"module": "$import(@zuplo/runtime)",
|
|
413
|
+
"options": {
|
|
414
|
+
"classifier": {
|
|
415
|
+
"app": {
|
|
416
|
+
"appId": "$env(CLASSIFIER_APP_ID)",
|
|
417
|
+
"adapter": "systemOne",
|
|
418
|
+
"model": "jev/jev-latest"
|
|
340
419
|
}
|
|
341
420
|
},
|
|
342
421
|
"smartRoutingEnabled": true,
|
|
@@ -346,6 +425,24 @@
|
|
|
346
425
|
"high": "openai/gpt-5"
|
|
347
426
|
}
|
|
348
427
|
}
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"export": "AIGatewaySmartRouterInboundPolicy",
|
|
431
|
+
"module": "$import(@zuplo/runtime)",
|
|
432
|
+
"options": {
|
|
433
|
+
"classifier": {
|
|
434
|
+
"app": {
|
|
435
|
+
"appId": "$env(CLASSIFIER_APP_ID)",
|
|
436
|
+
"adapter": "chatCompletions",
|
|
437
|
+
"model": "openai/gpt-4o-mini"
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
"modelsByComplexity": {
|
|
441
|
+
"low": "openai/gpt-4o-mini",
|
|
442
|
+
"medium": "openai/gpt-4o",
|
|
443
|
+
"high": "openai/gpt-5"
|
|
444
|
+
}
|
|
445
|
+
}
|
|
349
446
|
}
|
|
350
447
|
]
|
|
351
448
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Add this policy first in the inbound policies of each route that only your
|
|
2
|
+
gateway calls. With no options it rejects requests from outside the gateway with
|
|
3
|
+
`403 Forbidden`; set `status`, `statusText`, and `detail` to change the
|
|
4
|
+
response.
|
|
5
|
+
|
|
6
|
+
## How it works
|
|
7
|
+
|
|
8
|
+
`context.invokeRoute` runs a route on this gateway without leaving the process,
|
|
9
|
+
and the runtime marks the request it creates by setting `context.parentContext`
|
|
10
|
+
to the calling request's context. Nothing else sets that property. A request
|
|
11
|
+
that arrives over the network never has one, whatever headers, URL, or body it
|
|
12
|
+
carries. This policy passes a request when `context.parentContext` is present
|
|
13
|
+
and rejects it otherwise.
|
|
14
|
+
|
|
15
|
+
| Request origin | Result |
|
|
16
|
+
| ------------------------------------------------------------------------------ | -------- |
|
|
17
|
+
| Any HTTP client, with or without credentials | Rejected |
|
|
18
|
+
| `context.invokeRoute` from a policy, handler, or custom module on this gateway | Passes |
|
|
19
|
+
| The MCP Server handler calling the route as a tool | Passes |
|
|
20
|
+
| An AI Gateway application calling another application on the same gateway | Passes |
|
|
21
|
+
|
|
22
|
+
The policy does not authenticate the caller and does not set `request.user`.
|
|
23
|
+
|
|
24
|
+
## Choose the response
|
|
25
|
+
|
|
26
|
+
By default, a rejected request gets a `403 Forbidden` problem response with no
|
|
27
|
+
`detail`, so the caller learns nothing about how your gateway works. The policy
|
|
28
|
+
writes the reason to the request log at debug level, where you can see it and
|
|
29
|
+
the caller can't.
|
|
30
|
+
|
|
31
|
+
Set `status` to answer with any client error status from `400` to `499`,
|
|
32
|
+
`statusText` to replace the status text, and `detail` to add a message:
|
|
33
|
+
|
|
34
|
+
| `options` | Response to a request from outside the gateway |
|
|
35
|
+
| -------------------------------------------------- | -------------------------------------------------------------------- |
|
|
36
|
+
| `{}` | `403 Forbidden` with no `detail` |
|
|
37
|
+
| `{ "status": 404 }` | `404 Not Found` with no `detail`, as for a route that does not exist |
|
|
38
|
+
| `{ "status": 401 }` | `401 Unauthorized` with no `detail` |
|
|
39
|
+
| `{ "status": 401, "statusText": "Access Denied" }` | `401 Access Denied` with no `detail` |
|
|
40
|
+
| `{ "detail": "Access denied." }` | `403 Forbidden` with your `detail` |
|
|
41
|
+
|
|
42
|
+
Every caller outside the gateway receives your `detail`, so don't use it to
|
|
43
|
+
explain how the route is called or what protects it.
|
|
44
|
+
|
|
45
|
+
`statusText` replaces only the status text; the problem's `title` stays the
|
|
46
|
+
standard one, such as `Unauthorized`. Clients don't always receive it: HTTP/2
|
|
47
|
+
and HTTP/3 have no status text, and Managed Dedicated and self-hosted gateways
|
|
48
|
+
always send the standard text.
|
|
49
|
+
|
|
50
|
+
## Hide REST routes behind an MCP server
|
|
51
|
+
|
|
52
|
+
The MCP Server handler calls each tool's route with `context.invokeRoute`, so
|
|
53
|
+
the tools keep working while direct calls to those REST routes get the same
|
|
54
|
+
`404` as a route that does not exist. Declare the policy with `"status": 404`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"name": "internal-only-inbound",
|
|
59
|
+
"policyType": "internal-only-inbound",
|
|
60
|
+
"handler": {
|
|
61
|
+
"export": "InternalOnlyInboundPolicy",
|
|
62
|
+
"module": "$import(@zuplo/runtime)",
|
|
63
|
+
"options": {
|
|
64
|
+
"status": 404
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Put it first in the inbound policies of each REST route that the MCP server
|
|
71
|
+
exposes, so nothing else runs for a rejected request. Mark the route
|
|
72
|
+
[`x-internal`](/docs/articles/openapi#x-internal) to keep it out of your
|
|
73
|
+
developer portal as well:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
"/orders/{orderId}": {
|
|
77
|
+
"get": {
|
|
78
|
+
"operationId": "get-order",
|
|
79
|
+
"x-internal": true,
|
|
80
|
+
"parameters": [
|
|
81
|
+
{
|
|
82
|
+
"name": "orderId",
|
|
83
|
+
"in": "path",
|
|
84
|
+
"required": true,
|
|
85
|
+
"schema": { "type": "string" }
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"x-zuplo-route": {
|
|
89
|
+
"corsPolicy": "none",
|
|
90
|
+
"handler": {
|
|
91
|
+
"export": "urlForwardHandler",
|
|
92
|
+
"module": "$import(@zuplo/runtime)",
|
|
93
|
+
"options": { "baseUrl": "https://orders.example.com" }
|
|
94
|
+
},
|
|
95
|
+
"policies": {
|
|
96
|
+
"inbound": ["internal-only-inbound"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The policy's `404` is the gateway's standard not-found problem. Two things can
|
|
104
|
+
still tell a hidden route from one that doesn't exist:
|
|
105
|
+
|
|
106
|
+
- Preflight `OPTIONS` requests skip inbound policies, so a route with a CORS
|
|
107
|
+
policy still answers them. Set `corsPolicy` to `none` on hidden routes, as in
|
|
108
|
+
the example.
|
|
109
|
+
- If you set `runtime.notFoundHandler` in your
|
|
110
|
+
[runtime extensions](/docs/programmable-api/runtime-extensions), paths that
|
|
111
|
+
don't exist answer with your handler's response instead.
|
|
112
|
+
|
|
113
|
+
The MCP server route lists the operation as a tool and authenticates its own
|
|
114
|
+
callers:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
"/mcp": {
|
|
118
|
+
"post": {
|
|
119
|
+
"x-zuplo-route": {
|
|
120
|
+
"handler": {
|
|
121
|
+
"export": "mcpServerHandler",
|
|
122
|
+
"module": "$import(@zuplo/runtime)",
|
|
123
|
+
"options": {
|
|
124
|
+
"operations": [{ "file": "./config/routes.oas.json", "id": "get-order" }]
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"policies": {
|
|
128
|
+
"inbound": ["api-key-inbound"]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Protect an AI Gateway application
|
|
136
|
+
|
|
137
|
+
New AI Gateway projects declare this policy as `internal-only-inbound`. Select
|
|
138
|
+
it in an application's `inboundPolicyChain` in place of
|
|
139
|
+
`ai-gateway-auth-inbound`, and put it first so nothing else runs for a rejected
|
|
140
|
+
request:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"inboundPolicyChain": [
|
|
145
|
+
{ "name": "internal-only-inbound" },
|
|
146
|
+
{ "name": "ai-gateway-model-filtering-inbound" },
|
|
147
|
+
{ "name": "ai-gateway-metering-inbound" }
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A chain entry's `options` replace the declared options for that application
|
|
153
|
+
only:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{ "name": "internal-only-inbound", "options": { "status": 404 } }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
AWS Bedrock Runtime requests get the AWS error envelope instead of a problem
|
|
160
|
+
response, so the AWS SDKs can parse the error: `AccessDeniedException` for a
|
|
161
|
+
`401` or `403`, and `ResourceNotFoundException` for a `404`. Its `message` is
|
|
162
|
+
your `detail`, or the status title, such as `Forbidden`, when you set none. That
|
|
163
|
+
holds in an application's chain and on the AI Gateway route itself, ahead of the
|
|
164
|
+
configuration loader.
|
|
165
|
+
|
|
166
|
+
Metering records these requests against the application with no consumer, and a
|
|
167
|
+
budget rule keyed on `request.user` has no value to accumulate for them. Attach
|
|
168
|
+
per-consumer budgets to the calling application instead.
|
|
169
|
+
|
|
170
|
+
The Smart Router and Prompt Injection Protection policies call their classifier
|
|
171
|
+
applications this way.
|
|
172
|
+
|
|
173
|
+
## Call a protected route
|
|
174
|
+
|
|
175
|
+
Call the route from a policy or handler on the same gateway. No credentials are
|
|
176
|
+
needed:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const response = await context.invokeRoute("/orders/1234");
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
A direct call from outside the gateway is rejected:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
curl https://gateway.example.com/orders/1234
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
With the default options the response is `403 Forbidden` with no `detail`. The
|
|
189
|
+
request log records why the policy rejected it.
|
|
190
|
+
|
|
191
|
+
## What it does not check
|
|
192
|
+
|
|
193
|
+
This policy asserts that a request originated inside this gateway deployment,
|
|
194
|
+
not that the caller is trusted. Any route on the same gateway that forwards
|
|
195
|
+
requests through `context.invokeRoute`, including an MCP server that exposes the
|
|
196
|
+
route as a tool, reaches it. Protect those routes with their own authentication,
|
|
197
|
+
exactly as you would an internal URL.
|
|
198
|
+
|
|
199
|
+
## Deprecated names
|
|
200
|
+
|
|
201
|
+
This policy began in the AI Gateway as Ensure Gateway Internal Invocation Only.
|
|
202
|
+
Its earlier names are deprecated: the `AIGatewayInternalOnlyInboundPolicy`
|
|
203
|
+
export, the `ai-gateway-internal-only` policy type, and the
|
|
204
|
+
`ai-gateway-internal-only-inbound` chain-entry name. Configurations that use
|
|
205
|
+
them keep working unchanged, but new configurations should use
|
|
206
|
+
`InternalOnlyInboundPolicy` and `internal-only-inbound`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
The Internal Only policy admits a request only when this gateway made it with
|
|
2
|
+
`context.invokeRoute`, and rejects every request that arrived over the network.
|
|
3
|
+
|
|
4
|
+
Use it on routes that only other routes, handlers, and policies on the same
|
|
5
|
+
gateway call: REST routes that an MCP server exposes as tools, or the classifier
|
|
6
|
+
application behind the Smart Router or Prompt Injection Protection policies.
|
|
7
|
+
Those calls never leave the gateway, so an API key on them protects nothing. A
|
|
8
|
+
rejected request gets `403 Forbidden` by default; set `status` to `404` to
|
|
9
|
+
answer as if the route did not exist. The policy makes no network calls and
|
|
10
|
+
never reads the request body.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "https://cdn.zuplo.com/policies/runtime/schemas/internal-only-inbound.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"title": "Internal Only",
|
|
6
|
+
"isDeprecated": false,
|
|
7
|
+
"isPaidAddOn": false,
|
|
8
|
+
"isEnterprise": false,
|
|
9
|
+
"isInternal": false,
|
|
10
|
+
"isBeta": false,
|
|
11
|
+
"isHidden": false,
|
|
12
|
+
"requiresAI": false,
|
|
13
|
+
"policyType": "internal-only-inbound",
|
|
14
|
+
"products": ["api-gateway", "ai-gateway"],
|
|
15
|
+
"description": "Accepts only requests that this gateway made itself with `context.invokeRoute`, and rejects every request that arrived over the network.\n\nUse it on a route that only other routes, handlers, and policies on the same gateway call: a REST endpoint that an MCP server exposes as a tool, or 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 asserts the one thing that matters: the request originated inside the deployment.\n\nThe check reads `context.parentContext`, which the runtime sets only on the child context that `context.invokeRoute` creates. A client cannot supply it through a header, the URL, or the body. The policy makes no network calls, reads no body, and leaves `request.user` unset.\n\nA rejected request gets a `403 Forbidden` problem response with no `detail` by default, so the caller learns nothing about how the gateway works; the reason goes to the request log. Set `status` to answer with another client error, such as `404` to hide the route or `401`, `statusText` to change the status text, and `detail` to add a message.",
|
|
16
|
+
"deprecatedMessage": "",
|
|
17
|
+
"required": ["handler"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"handler": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"default": {},
|
|
22
|
+
"required": ["export", "module", "options"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"export": {
|
|
25
|
+
"const": "InternalOnlyInboundPolicy",
|
|
26
|
+
"description": "The name of the exported type"
|
|
27
|
+
},
|
|
28
|
+
"module": {
|
|
29
|
+
"const": "$import(@zuplo/runtime)",
|
|
30
|
+
"description": "The module containing the policy"
|
|
31
|
+
},
|
|
32
|
+
"options": {
|
|
33
|
+
"title": "InternalOnlyInboundPolicyOptions",
|
|
34
|
+
"type": "object",
|
|
35
|
+
"description": "The options for this policy.",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": [],
|
|
38
|
+
"properties": {
|
|
39
|
+
"status": {
|
|
40
|
+
"type": "integer",
|
|
41
|
+
"minimum": 400,
|
|
42
|
+
"maximum": 499,
|
|
43
|
+
"default": 403,
|
|
44
|
+
"description": "The HTTP status code returned to a request that did not come from inside the gateway. Any client error status from 400 to 499 is allowed. Use `404` to answer as if the route did not exist, or `401` to answer as an authentication policy does."
|
|
45
|
+
},
|
|
46
|
+
"statusText": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1,
|
|
49
|
+
"pattern": "^[\\t\\x20-\\x7E\\x80-\\xFF]+$",
|
|
50
|
+
"examples": ["Not Found", "Unauthorized"],
|
|
51
|
+
"x-advanced": true,
|
|
52
|
+
"description": "The status text returned with `status`, such as `Not Found`. When it is omitted, the response uses the standard text for the status. Clients don't always receive it: HTTP/2 and HTTP/3 have no status text, and Managed Dedicated and self-hosted gateways always send the standard text."
|
|
53
|
+
},
|
|
54
|
+
"detail": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"minLength": 1,
|
|
57
|
+
"x-advanced": true,
|
|
58
|
+
"description": "The `detail` of the problem response returned to a rejected request. Every caller outside the gateway receives it, so don't describe how the route is called. When it is omitted, the response has no `detail`, and a `404` matches the response for a route that does not exist."
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"examples": [
|
|
64
|
+
{
|
|
65
|
+
"export": "InternalOnlyInboundPolicy",
|
|
66
|
+
"module": "$import(@zuplo/runtime)",
|
|
67
|
+
"options": {
|
|
68
|
+
"status": 403,
|
|
69
|
+
"statusText": "Not Found"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|