Gateway
Live control of the policy your API applies to every incoming request: CORS, rate limits, cache TTL and allowed methods, changed without a redeploy.
Also for this capability: API · CLI · MCP · SDKs
Live control of the policy your API applies to every incoming request: CORS, rate limits, cache TTL and allowed methods, changed without a redeploy.
| Base URL | https://api.hanzo.ai |
| Operations | 3 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1127 · Gateway — Live Edge Policy — Draft · read the specification →
/v1/gateway is live control of the policy the API applies to every incoming
request — CORS, rate limits, cache TTL, allowed methods — changed without a
redeploy. The gateway itself is plumbing: the trust boundary that validates the
IAM JWT, strips client-supplied identity and re-mints the org header, compiled
into the cloud binary rather than deployed as a network hop
(apps/gateway/gateway.go:5-9). Plumbing earns no prefix; what earns this one
is the thing a customer actually calls, the runtime config plane, implemented
in hanzoai/cloud at apps/gateway.
Motivation
The edge knobs used to be baked into an image, so retuning a CORS allowlist or
a flood cap was a rebuild and a rollout — the slowest possible response to the
fastest-moving class of problem. The config plane serves GET/PUT over the same
store the edge middleware reads live (apps/gateway/gateway.go:11-16), so a
change is effective on the next request.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The store is shared, not owned
The policy store is owned by the composition root (deps.GatewayPolicy) and
shared with the enforcement middleware; this subsystem opens nothing and closes
nothing (apps/gateway/gateway.go:37-40, gateway.go:80-82). One store, one
source of truth: the row an operator writes here is the row the edge evaluates,
with no propagation step to fail. Per-project rate scoping is deliberately not
here — it stays the commerce-configured domain of the scoped limiter.
§2 The addresses
Three typed operations, the whole surface (apps/gateway/gateway.go:104-110,
manifest/apps.go:365): GET /v1/gateway/config, PUT /v1/gateway/config,
and GET /v1/gateway/traffic, which reports who is calling the org's API right
now.
§3 Two scopes, and the one field that crosses them
Policy splits on whether a tenant exists at evaluation time
(apps/gateway/gateway.go:18-35):
- Platform policy — CORS origins, the pre-auth per-IP cap and window — is evaluated before any tenant is known and MUST be writable only by a SuperAdmin. A PUT carrying any platform field routes to the platform row explicitly, so it lands correctly even when the SuperAdmin is org-switched.
- Per-org policy — the authenticated rate ceiling, cache TTL and paths,
the accepted-method allowlist — is a tenant's own self-service row. An org
admin writes its own, with the org from the validated principal
(
apps/gateway/gateway.go:122, HIP-0026), never a raw header; a SuperAdmin MAY target any tenant by query. - Mode — the abuse gate's posture — lives on a tenant's row but is NOT self-service: a control's subject may not switch the control off, so writing it requires SuperAdmin whichever row it lands on. It is the one field whose scope and whose authority are different questions.
§4 Money, events, telemetry
gateway is free, in those words (plugin/gateway/main.go:21, cloud.Free; not
in spend.go:275). It publishes no events on the bus, and emits nothing to
observability beyond the request span every route gets — the traffic operation
is a read of the edge's live counters, not an emission.
§5 Stage
gateway is ga: the platform core's edge, part of the agentic OS.
§6 Upstream
gateway derives from none. hanzoai/gateway is the Hanzo repository the trust
boundary lives in, and its own routing law — one routing source of truth,
cloud's mount table, never a second map — is why this capability is a config
plane and not a router (apps/gateway/gateway.go:8-10).
Rationale
One shared store read live, rather than a config service the edge polls, means there is no window in which the operator's view and the enforced policy disagree — the alternative's failure mode is precisely the one an abuse response cannot afford. Splitting authority by evaluation time, rather than by field list alone, gives the rule a reason the next field can be tested against: if no tenant exists when the knob is evaluated, no tenant may turn it.
Security Considerations
This surface configures the defenses, so the wrong implementation disarms them. The three failure shapes are each closed by a scope rule: a tenant widening platform CORS or the pre-auth flood cap (platform fields are SuperAdmin-only), a tenant raising another tenant's ceiling (the org comes from the validated principal, and cross-org targeting requires SuperAdmin), and an abuser switching off their own abuse gate (mode requires SuperAdmin on any row). The store being deps-owned also means a compromised subsystem cannot substitute a second policy source — there is nothing here to swap.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | gateway at its own prefix | 3 operations |
| CLI | hanzo gateway … | 3 of 3 |
| SDK | GatewayApi in every published client | 3 methods |
| MCP | tool gateway on https://api.hanzo.ai/v1/mcp | 3 operations, 2 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/gateway/config, operation get_gateway_config:
hanzo gateway config getimport { Configuration, GatewayApi } from 'hanzoai';
const api = new GatewayApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getGatewayConfig();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import GatewayApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = GatewayApi(client).get_gateway_config()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.GatewayAPI.GetGatewayConfig(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, gateway_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = gateway_api::get_gateway_config(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.GatewayApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new GatewayApi(client).getGatewayConfig();curl https://api.hanzo.ai/v1/gateway/config \
-H "Authorization: Bearer $HANZO_API_KEY"Tool gateway, op get_gateway_config — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "gateway",
"arguments": {
"op": "get_gateway_config",
"input": {}
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/gateway/config | Read returns the EFFECTIVE edge policy the caller is subject to: the platform CORS allowlist and pre-auth per-IP flood cap in force, plus the… |
PUT /v1/gateway/config | Write updates one policy scope and returns the policy in force after the write. |
GET /v1/gateway/traffic | Report who is calling this org's API right now |
How is this guide?