Network Operations Guide

For: Network operations and platform teams configuring policies, managing credentials, and monitoring Axemere Gateway health.

Table of Contents


Admin API Authentication

All admin endpoints require one of:

  • MVGC-Admin-Token: <token> header
  • Authorization: Bearer <token> header

The token value is set via the MVGC_ADMIN_TOKEN environment variable on the gateway. In a Docker Compose deployment it can also be found in docker-compose.yaml.


Policy Configuration

Policies are defined as YAML files under MVGC_POLICIES_DIR (default configs/policies/). A policy bundle (mvp_policy.yaml) references individual rule files that are evaluated in a defined layer order.

Evaluation Layers

allow

deny

rate_limit

require_approval

quarantine

Incoming Request

1. identity
validate caller and ingress mode

2. delegation
validate delegation tokens

3. connectors
select connector based on target

4. targets
allowlist / denylist target hosts

5. credentials
select credential for the request

6. budgets
enforce cost caps and daily limits

7. risk
risk scoring and anomaly detection

8. transforms
mutate the request (e.g. model downgrade)

Final decision

Execute and record

HTTP 403

HTTP 429 + Retry-After

HTTP 202 + approval_id

HTTP 403 (quarantined)

Evaluation stops on the first deny or require_approval match by default (stop_on is configurable per bundle). The default decision when no rule matches is deny.

Supported Operators

OperatorDescription
equalsExact string match
inValue is in a list
not_inValue is not in a list
existsField is present and non-empty
regexRegular expression match
prefixString has given prefix
suffixString has given suffix
lt, lte, gt, gteNumeric comparisons

Logical combinators: all (AND), any (OR). These can nest to arbitrary depth.

Decisions

DecisionHTTP StatusDescription
allow200Permit the request
deny403Block the request with a reason
downgrade200Allow with mutations (e.g. swap to a cheaper model)
require_approval202Hold for manual approval; returns approval_id
require_attribution403Block the request; required attribution fields are missing (see require_attribution)
rate_limit429Reject with Retry-After header
quarantine403Block and quarantine for review due to risk signals

Set MVGC_APPROVAL_ENABLED=false to revert require_approval to the legacy 403 behavior.

require_attribution and stop_on: The require_attribution decision is included in the bundle stop_on list (alongside deny and require_approval), so evaluation halts immediately when a rule produces it. Use effect.decision: require_attribution in identity or delegation layer rules to block requests that are missing required attribution fields before further evaluation proceeds.

Hot-Loading a Policy Bundle

Push a bundle that references rule files already present on the gateway filesystem under MVGC_POLICIES_DIR:

curl -X PUT http://localhost:7080/v1/admin/policies \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/yaml" \
  --data-binary @configs/examples/mvp_policy.yaml

Push a fully self-contained bundle with rules embedded inline (no files need to exist on the filesystem). inline_rules takes precedence over files when both are present:

curl -X PUT http://localhost:7080/v1/admin/policies \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/yaml" \
  --data-binary @- << 'EOF'
schema: mvgc.policy_bundle.v1
bundle_id: bundle-inline-001
version: 1.0.0
defaults:
  decision: deny
evaluation:
  order: [identity, targets, credentials, budgets, risk]
  stop_on: [deny, require_approval]
  merge_strategy: first_match
inline_rules:
  identity:
    - id: identity.allow.all.explicit
      priority: 100
      when:
        field: context.connection_type
        equals: direct_api
      effect:
        decision: allow
  risk:
    - id: risk.require_approval.high_cost
      priority: 100
      when:
        field: context.action.params.risk_tier
        equals: "high"
      effect:
        decision: require_approval
        reason: "risk tier requires manual approval"
EOF

Both variants respond with {status, bundle_id, version} on success.

Reloading Policies from Disk (including add-ons)

If you add, remove, or enable a new policy add-on by placing or symlinking a file into configs/policies/addons/ while the gateway is running, use the disk reload endpoint to pick up the changes without restarting:

curl -s -X POST http://localhost:7080/v1/admin/policies/reload \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

Response:

{
  "status":    "ok",
  "bundle_id": "bundle-prod-v2",
  "version":   "2.1.0",
  "source":    "file"
}

This re-reads bundle.yaml and all files in addons/ from MVGC_POLICIES_DIR. Any previously API-pushed override (from PUT /v1/admin/policies) is cleared; the gateway returns to reading from disk on every subsequent request evaluation. Returns 400 if the gateway is running in distributed (CP remote) mode, where bundles are managed by the control plane.

Add-on workflow: copy or symlink the add-on from configs/policies/available/ into configs/policies/addons/, then call this endpoint. No restart needed.

Verifying the Active Policy Bundle

Use GET /v1/admin/policies to confirm which bundle is currently loaded; useful immediately after a reload to verify it took effect:

curl -s http://localhost:7080/v1/admin/policies \
  -H "MVGC-Admin-Token: $ADMIN_TOKEN" | jq .

Response:

{
  "bundle_id": "bundle-prod-v2",
  "version":   "2.1.0",
  "source":    "override",
  "loaded_at": "2026-03-15T14:22:10Z"
}
FieldDescription
bundle_idThe bundle_id from the active bundle YAML
versionThe version field from the active bundle
source"override": hot-loaded via PUT; "file": loaded from MVGC_POLICIES_DIR on disk (including after a POST /reload); "default": built-in deny-all fallback
loaded_atISO-8601 timestamp when the override was activated; omitted for file and default sources

See configs/policies/available/ for the add-on catalog and configs/policies/ for the per-layer rule files used with mvp_policy.yaml. Use POST /v1/admin/policies/reload to pick up new add-ons without restarting.


Proxy Mode Credential Setup

When proxy_enabled: true, the gateway acts as a forwarding proxy -- clients send HTTP traffic to the gateway, which runs policy evaluation and injects credentials before forwarding to the upstream provider. API keys are not injected automatically; you must register credentials and load a policy bundle that explicitly selects them per target host.

The four required steps are described below. A ready-to-use example bundle is provided at configs/examples/proxy-credentials.yaml.

Step 1 -- Register credentials

Register one credential per provider. The secret_ref names the environment variable that holds the actual API key -- the key itself is never stored in the database.

# OpenAI
curl -X PUT http://localhost:7080/v1/admin/credentials \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"credential_id":"cred-openai","provider":"openai","mode":"alias",
       "billing_owner":"platform","secret_ref":"OPENAI_API_KEY"}'

# Anthropic
curl -X PUT http://localhost:7080/v1/admin/credentials \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"credential_id":"cred-anthropic","provider":"anthropic","mode":"alias",
       "billing_owner":"platform","secret_ref":"ANTHROPIC_API_KEY"}'

Repeat for every provider you need. See Key Alias for the full field reference.

Step 2 -- Set the API key environment variables

Set the environment variable named in each secret_ref before (or after, via a restart) starting the gateway. In mvgc.yaml there is no direct place for secrets; use environment variables or a secrets manager:

export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...

For Homebrew deployments, add these to your shell profile or the launchd environment before running brew services start mvgc-gateway.

Step 3 -- Load a policy bundle with select_credential rules

The policy connectors layer is the right place to bind credentials to target hosts. Load the example bundle from configs/examples/proxy-credentials.yaml, or push an inline bundle directly:

curl -X PUT http://localhost:7080/v1/admin/policies \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/yaml" \
  --data-binary @- << 'EOF'
schema: mvgc.policy_bundle.v1
bundle_id: proxy-with-creds
version: "1.0.0"
layer_order: [connectors]
inline_rules:
  connectors:
    - id: proxy.allow.openai
      priority: 100
      when:
        field: action.target_host
        equals: api.openai.com
      effect:
        decision: allow
        select_credential: cred-openai
    - id: proxy.allow.anthropic
      priority: 100
      when:
        field: action.target_host
        equals: api.anthropic.com
      effect:
        decision: allow
        select_credential: cred-anthropic
    - id: proxy.deny.unknown-host
      priority: 500
      when:
        field: action.target_host
        exists: true
      effect:
        decision: deny
        reason: "no credential configured for this target host"
EOF

The default decision is deny, so any target host not matched by an explicit rule will be blocked. Add a rule for each provider you want to allow.

Step 4 -- Verify the setup

Confirm the bundle is loaded and credentials are registered:

# Active bundle
curl -s http://localhost:7080/v1/admin/policies \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

# Registered credentials (keys are never returned)
curl -s http://localhost:7080/v1/admin/credentials \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

Step 5 -- Test a proxied request

Send a request through the proxy using the /proxy/{provider}/ path prefix to identify the upstream provider. The gateway resolves openai to api.openai.com automatically:

curl http://localhost:7080/proxy/openai/v1/chat/completions \
  -H "X-MVGC-Org-ID: my-org" \
  -H "X-MVGC-Workload-ID: my-app" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4","messages":[{"role":"user","content":"hello"}]}'

The gateway evaluates policy, injects the OPENAI_API_KEY from the environment, and forwards the request. The caller never sees the API key.

Supported path-prefix providers: openai, anthropic, gemini, cohere. For Azure OpenAI or any target not in this list, pass the X-MVGC-Target-Host header explicitly:

curl http://localhost:7080/v1/chat/completions \
  -H "X-MVGC-Org-ID: my-org" \
  -H "X-MVGC-Workload-ID: my-app" \
  -H "X-MVGC-Target-Host: my-resource.openai.azure.com" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4","messages":[{"role":"user","content":"hello"}]}'

Target host resolution order: (1) X-MVGC-Target-Host header, (2) request Host header, (3) /proxy/{provider}/ path prefix, (4) 400 error if none of the above resolves.

Delegation tokens in proxy mode: In transparent proxy mode, the X-MVGC-Delegation-ID header carries only the token ID -- the signed payload is not transmitted. Delegation scope constraints are therefore not enforced. For full scope enforcement, use explicit POST /v1/actions:execute with the delegation_token field.


Common Policy Patterns

Below are concrete YAML examples for frequent policy use cases. Each rule goes in the appropriate layer file under MVGC_POLICIES_DIR or inline in a bundle via inline_rules.

Allow only specific models:

targets:
  - id: targets.deny.disallowed_models
    priority: 100
    when:
      field: context.action.params.model
      not_in: ["gpt-4o-mini", "claude-3-haiku-20240307"]
    effect:
      decision: deny
      reason: "model not in approved list"

Rate-limit a workload to 10 requests per minute:

identity:
  - id: identity.rate_limit.wl_prod_app
    priority: 90
    when:
      field: context.workload_id
      equals: "wl-prod-app-1"
    effect:
      decision: rate_limit
      reason: "workload rate limit: 10 req/min"

The token bucket refill rate is controlled by the policy decision; the rate_limit decision triggers the gateway's built-in token bucket limiter keyed by workload_id.

Require approval for high-cost requests:

budgets:
  - id: budgets.require_approval.high_cost
    priority: 80
    when:
      field: context.action.params.model
      in: ["gpt-4o", "claude-3-5-sonnet-20241022"]
    effect:
      decision: require_approval
      reason: "high-cost model requires operator approval"

Deny requests outside business hours (combined with risk):

risk:
  - id: risk.deny.outside_hours
    priority: 70
    when:
      field: context.risk.signals
      # time_of_day signal active means outside MVGC_RISK_BUSINESS_HOURS
      in: ["time_of_day"]
    effect:
      decision: deny
      reason: "requests not permitted outside business hours"

Downgrade model on budget pressure:

transforms:
  - id: transforms.downgrade.budget_pressure
    priority: 60
    when:
      field: context.risk.signals
      in: ["cost_anomaly"]
    effect:
      decision: downgrade
      mutations:
        - key: action.params.model
          value: "gpt-4o-mini"

Credential Configuration

BYOK (Bring Your Own Key)

The caller supplies the API key in the request via credential_hint. The gateway passes it through to the provider without storing it. No server-side configuration is required.

Use byok when callers bring their own provider accounts and the gateway should not manage keys.

Key Alias

The gateway manages API keys server-side. Credentials are registered with a secret_ref pointing to an environment variable. The caller references a credential_id and the gateway resolves the secret at runtime.

Credential fields:

FieldRequiredDescription
credential_idYesUnique identifier for the credential
providerYesProvider name (e.g. openai, anthropic, gemini)
modeYes"alias" for key alias mode
billing_ownerYes"customer" or "platform"
secret_refYesEnvironment variable name holding the API key
org_idNoScopes the credential to a specific organization; when empty, available to all orgs
connector_idNoBinds this credential to a specific connector ID (e.g. "openai"); enforced at credential metadata level
scopesNoHuman-readable scope list (e.g. ["chat", "embeddings"]) for auditing and future machine-policy use

Register an alias credential:

curl -X PUT http://localhost:7080/v1/admin/credentials \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "credential_id": "cred-openai",
    "provider": "openai",
    "mode": "alias",
    "billing_owner": "customer",
    "secret_ref": "OPENAI_API_KEY",
    "org_id": "org-example-001",
    "connector_id": "openai",
    "scopes": ["chat", "embeddings"]
  }'

The org_id field scopes the credential to a specific organization for multi-tenant deployments. When set, the credential is only resolvable by requests matching that org_id. When empty (default), the credential is available to all organizations.

The value of OPENAI_API_KEY must be set in the gateway's environment. The calling application never sees the API key -- it references only the credential ID.

List registered credentials:

curl -X GET http://localhost:7080/v1/admin/credentials \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

Delete a credential:

curl -X DELETE http://localhost:7080/v1/admin/credentials/cred-openai \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN"
# 204 No Content on success; 404 if credential not found

Reloading credentials from file

If you update your credentials.yaml on disk after the gateway is already running (e.g. to change an inline key or switch modes), the file changes are not picked up automatically; the gateway seeds from the file only on first boot when the database is empty.

Use the reload endpoint to apply your current file to the running gateway without a restart. This does a full upsert (overwriting any existing DB values) for every credential defined in the file:

curl -s -X POST http://localhost:7080/v1/admin/credentials/reload \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

Response:

{
  "count": 5,
  "status": "ok",
  "upserted": ["cred-anthropic", "cred-azure-openai", "cred-cohere", "cred-gemini", "cred-openai"]
}

The gateway reads all *.yaml files from MVGC_CREDENTIALS_DIR and upserts each credential. If any file fails to parse or any upsert fails, the response is 400/500 with an error message and the remaining credentials are not processed (fix the error and re-run).

See configs/credentials/credentials.yaml for examples. The seed directory is configurable via MVGC_CREDENTIALS_DIR (default: configs/credentials; deb/rpm: /etc/mvgc/credentials).


Workload Management

Workloads represent logical applications or services that submit requests through the gateway. Each workload carries default_attribution fields (used when the caller omits them) and an allowed_connection_types list that controls which connection types the workload may use.

Workload Fields

FieldRequiredDescription
workload_idYesUnique identifier for the workload (see workload_id)
org_idYesThe organization this workload belongs to
nameNoHuman-readable display name
default_attributionNoFallback attribution fields when the caller omits them
allowed_connection_typesNoList of permitted connection types (direct_api, sdk_redirect, connect_proxy); if omitted, all are allowed

Creating or Updating a Workload

curl -X PUT http://localhost:7080/v1/admin/workloads \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "workload_id": "wl-prod-app-1",
    "org_id": "org-example-001",
    "name": "Production Application",
    "default_attribution": {
      "customer_id": "cust-default",
      "account_id": "acct-default"
    },
    "allowed_connection_types": ["direct_api"]
  }'

allowed_connection_types accepts any combination of direct_api, sdk_redirect, and connect_proxy. default_attribution fills in any attribution fields not supplied by the caller.

Listing Workloads

curl -s http://localhost:7080/v1/admin/workloads \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

Returns an array of all registered workloads with their workload_id, org_id, name, default_attribution, and allowed_connection_types.

Deleting a Workload

curl -X DELETE http://localhost:7080/v1/admin/workloads/wl-prod-app-1 \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN"

Returns 204 No Content on success, 404 Not Found if the workload does not exist.

Alternatively, to soft-decommission without deleting, update allowed_connection_types to [] (empty list) to block all requests while retaining the workload record.


Connector Management

Connectors are stateless route handlers that map action.target_host values to upstream AI provider endpoints. The gateway ships with built-in connectors for OpenAI, Anthropic, Gemini, Azure OpenAI, and a generic HTTP fallback.

The connectors policy layer is evaluated before targets. In the default add-on configuration, provider routing rules in addons/provider-*.yaml and addons/deny-unknown-host.yaml handle this layer. For custom connector-level rules (e.g., allowlisting by connector ID), add rules to the connectors layer via an add-on:

connectors:
  - id: connectors.deny.non_openai
    priority: 100
    when:
      field: context.candidates.connectors
      not_in: ["openai", "anthropic"]
    effect:
      decision: deny
      reason: "connector not in approved list"

Interpreting Connector Health

GET /v1/admin/connectors returns a list of registered connectors with metadata and health status:

curl -s http://localhost:7080/v1/admin/connectors \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

Example response:

[
  {"connector_id": "openai",      "provider": "openai",    "target_host": "api.openai.com",    "status": "ok"},
  {"connector_id": "anthropic",   "provider": "anthropic", "target_host": "api.anthropic.com", "status": "ok"},
  {"connector_id": "gemini",      "provider": "gemini",    "target_host": "generativelanguage.googleapis.com", "status": "ok"},
  {"connector_id": "azure_openai","provider": "azure",     "target_host": "your-resource.openai.azure.com",    "status": "ok"},
  {"connector_id": "cohere",      "provider": "cohere",    "target_host": "api.cohere.com",    "status": "ok"},
  {"connector_id": "generic_http","provider": "generic",   "target_host": "(any)",             "status": "ok"}
]

SSE streaming: OpenAI, Anthropic, Gemini, Azure OpenAI, and Cohere connectors all support server-sent events (SSE) streaming when stream: true is set in action.params.

StatusMeaningAction
okConnector is registered and configuration looks correctNo action needed
no_credentialNo credential is configured or resolvable for this connectorRegister a credential via PUT /v1/admin/credentials
errorLast request to this connector returned an upstream errorCheck provider status page; verify the API key is valid

Connectors are stateless route handlers -- "health" reflects configuration state, not a live ping to the upstream provider.

Generic HTTP Connector Configuration

The generic HTTP connector (generic_http) supports header filtering via two complementary lists:

Config FieldDescription
AllowedHeadersPositive allowlist of header names to forward upstream. When non-empty, only headers in this list are forwarded. Evaluated before DeniedHeaders.
DeniedHeadersHeaders to strip before forwarding upstream (e.g. internal auth headers). Applied after AllowedHeaders filtering.

When both are configured, AllowedHeaders is evaluated first (only listed headers pass through), then DeniedHeaders removes any remaining unwanted headers. When AllowedHeaders is empty, all headers are forwarded except those in DeniedHeaders.

Action Type Routing

The connector manager supports routing by action.type in addition to hostname-based routing. Use RegisterActionType to map action type strings to connector IDs. This is step 2 in the routing precedence defined by spec §4.1:

  1. Policy-selected -- select_connector effect from a matching policy rule
  2. action.type -- type-based routing via registered mappings
  3. target_host -- hostname match against built-in provider connectors
  4. Fallback -- generic_http connector

Type mappings are registered programmatically in the gateway setup:

connectorManager.RegisterActionType("openai_chat", "openai")
connectorManager.RegisterActionType("custom.embedding", "embedding_service")

When a request's action.type matches a registered mapping, the request routes to the associated connector without requiring a hostname match. This enables custom action types that don't correspond to a specific provider hostname.

Custom action type mappings are registered during gateway initialization. Contact support if you need to add a custom action type mapping for a non-standard provider.


Node Inventory

The GET /v1/admin/nodes endpoint returns the list of gateway nodes (currently single-node):

curl -s http://localhost:7080/v1/admin/nodes \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
[
  {
    "node_id": "node-001",
    "registered_at": "2026-03-12T10:00:00Z",
    "mode": "embedded"
  }
]
FieldDescription
node_idGateway node identifier (from MVGC_NODE_ID)
registered_atWhen the node started (server boot time)
mode"embedded" (standalone) or "distributed" (connected to CP via gRPC)

Risk Scoring

The gateway computes a composite risk score (0.0 to 1.0) for each request based on four signals:

SignalWeightTrigger
rate_spike0.40Current request rate exceeds baseline by MVGC_RISK_RATE_THRESHOLD×
cost_anomaly0.30Current cost rate exceeds baseline by MVGC_RISK_COST_THRESHOLD×
target_diversity0.15Distinct target hosts in recent window exceeds baseline by MVGC_RISK_TARGET_DIVERSITY_THRESHOLD×
time_of_day0.15Request is outside configured MVGC_RISK_BUSINESS_HOURS

Baseline warm-up: rate_spike and cost_anomaly require at least 5 observations in the baseline window before they can fire. New workloads and fresh gateway restarts will not trigger these signals until enough history has accumulated. This prevents false positives during startup or low-traffic periods where any single request would otherwise appear anomalous against an empty baseline.

Risk context is available in DSL rules under the context.risk namespace:

DSL FieldDescription
context.risk.scoreComposite risk score (0.0-1.0)
context.risk.request_rateRequests per minute in the current window
context.risk.cost_rateEstimated cost per minute in the current window
context.risk.signalsArray of active risk signal names

Caller-supplied context is available under the context namespace:

DSL FieldDescription
context.purposeCaller intent string from ActionRequest.Context.Purpose
context.labels.<key>Value of a caller-supplied label (e.g. context.labels.env)

Example rule using caller context:

identity:
  - id: identity.deny.no_purpose
    priority: 100
    when:
      field: context.purpose
      operator: exists
      negate: true
    effect:
      decision: deny
      reason: "requests must declare a purpose"

Example rule that quarantines high-risk requests:

risk:
  - id: risk.quarantine.high_score
    priority: 100
    when:
      field: context.risk.score
      gt: "0.8"
    effect:
      decision: quarantine
      reason: "composite risk score too high"

The MVGC_RISK_* environment variables are fully wired into the execution flow. Risk scores are computed per-request and injected into the EvaluationContext before policy evaluation, so context.risk.* DSL conditions work in all policy layers. Configure the risk thresholds via environment variables -- see Observability and Risk Variables.

Delegation depth limit: Delegation chains deeper than 3 levels are denied before policy evaluation. The gateway returns a 403 with reason "delegation depth exceeds maximum (3)". This prevents unbounded delegation chains and limits blast radius if a delegation token is compromised.


Approval Workflows

When a policy rule returns require_approval, the gateway creates an approval request and returns HTTP 202 with an approval_id. The request is held until an operator approves or denies it. Approved requests are cached so subsequent identical requests execute normally.

policy returns require_approval
(HTTP 202 returned to caller)

POST /approve

POST /deny

MVGC_APPROVAL_TTL exceeded

subsequent request executes normally

subsequent request returns 403

pending

approved

denied

expired

Full lifecycle example:

Step 1 -- Submit a request that triggers require_approval:

curl -s -X POST http://localhost:7080/v1/actions:execute \
  -H "Content-Type: application/json" \
  -d '{
    "schema": "mvgc.action_request.v2",
    "request_id": "req-needs-approval",
    "org_id": "org-example-001",
    "workload_id": "wl-prod-app-1",
    "action": {
      "type": "ai.infer",
      "method": "POST",
      "target_host": "api.openai.com",
      "params": { "model": "gpt-4o", "risk_tier": "high" }
    },
    "attribution": { "project_id": "proj-123" }
  }' | jq .
# HTTP 202 -- response includes approval_id
# { "decision": "require_approval", "approval_id": "a1b2c3d4-...", ... }

Step 2 -- List pending approvals:

curl -s "http://localhost:7080/v1/admin/approvals?org_id=org-example-001&status=pending" \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

Step 3 -- Approve the request:

APPROVAL_ID="a1b2c3d4-..."   # from Step 1 response

curl -s -X POST "http://localhost:7080/v1/admin/approvals/$APPROVAL_ID/approve" \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"decided_by": "ops-admin"}' | jq .

Step 4 -- Re-submit the original request (now executes normally):

# Same body as Step 1 -- gateway finds the pre-approval and allows execution
curl -s -X POST http://localhost:7080/v1/actions:execute \
  -H "Content-Type: application/json" \
  -d '{ ... same body as Step 1 ... }' | jq .
# HTTP 200 -- { "decision": "allow", ... }

Deny instead of approve:

curl -s -X POST "http://localhost:7080/v1/admin/approvals/$APPROVAL_ID/deny" \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"decided_by": "ops-admin"}' | jq .

Quarantine Management

A quarantine policy decision returns HTTP 403 and records the entry server-side. Use the admin endpoints to review and release quarantined requests.

# List quarantined requests for an org
curl -s "http://localhost:7080/v1/admin/quarantine?org_id=org-example-001&limit=20" \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

# Release a quarantined entry
QUARANTINE_ID="q9z8y7x6-..."
curl -s -X POST "http://localhost:7080/v1/admin/quarantine/$QUARANTINE_ID/release" \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"released_by": "ops-admin"}' | jq .

The quarantine_id is not included in the execute response (the entry is written server-side); use the list endpoint to find and review quarantine entries.


Rate Limiting

When a request is rate-limited by policy, the gateway returns HTTP 429 with a Retry-After header indicating how many seconds to wait:

curl -v -X POST http://localhost:7080/v1/actions:execute \
  -H "Content-Type: application/json" \
  -d '{ ... }' 2>&1 | grep -E "< HTTP|Retry-After|decision"
# < HTTP/1.1 429 Too Many Requests
# < Retry-After: 60
# "decision": "rate_limit"

Rate limits are defined in policy DSL rules using the rate_limit decision. The token bucket implementation refills based on elapsed time and is keyed per workload.

Runtime Rate Limit Configuration

The rate limiter's token bucket parameters can be inspected and adjusted at runtime without restarting the gateway.

Get current configuration:

curl -s http://localhost:7080/v1/admin/ratelimit/config \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
{"refill_rate": 10, "max_tokens": 10, "window_duration": 60000000000}

Update configuration:

curl -s -X PUT http://localhost:7080/v1/admin/ratelimit/config \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"refill_rate": 20, "max_tokens": 50, "window_duration": 120000000000}' | jq .

Both refill_rate and max_tokens must be greater than 0. window_duration is in nanoseconds (e.g. 60000000000 = 1 minute).

List active bucket keys:

curl -s http://localhost:7080/v1/admin/ratelimit/keys \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

Returns an array of workload IDs that currently have tracked rate limit state:

["wl-prod-app-1", "wl-staging-2"]

Monitoring and Observability

Gateway Health (GET /healthz)

The GET /healthz endpoint returns enriched status information (no admin token required):

curl -s http://localhost:7080/healthz | jq .

Example response:

{
  "status": "ok",
  "node_id": "node-001",
  "connectors": [
    {"id": "openai", "version": "1.0.0", "healthy": true},
    {"id": "anthropic", "version": "1.0.0", "healthy": true}
  ],
  "cp_status": "connected",
  "bundle_id": "bundle-prod-001",
  "bundle_version": "2.1.0",
  "last_hash_submit_at": "2026-03-12T14:30:00Z"
}
FieldDescription
status"starting" during the startup grace period (MVGC_HEALTHZ_STARTUP_GRACE, default 10s), then "ok" once the gateway is fully initialized
node_idThis gateway's node identifier
connectorsArray of registered connectors with id, version, and healthy boolean
cp_statusControl plane connection: "connected", "offline", or "unconfigured"
bundle_idActive policy bundle ID (omitted when no bundle loaded)
bundle_versionActive policy bundle version (omitted when no bundle loaded)
last_hash_submit_atRFC3339 timestamp of the last successful hash submission to the CP ledger (omitted when not applicable)

Use cp_status to detect connectivity issues with the control plane in distributed mode. Use last_hash_submit_at to detect stale hash submission pipelines.

Prometheus Metrics

Prometheus metrics are available at GET /metrics when MVGC_METRICS_ENABLED=true (default).

curl -s http://localhost:7080/metrics | grep mvgc_

Example output:

mvgc_requests_total{action_type="ai.infer",decision="allow",org_id="org-example-001"} 42
mvgc_request_duration_ms_bucket{action_type="ai.infer",decision="allow",le="100"} 38
mvgc_risk_score{org_id="org-example-001"} 0.32
mvgc_approvals_pending 3
mvgc_rate_limit_hits{org_id="org-example-001"} 1
mvgc_quarantine_total{org_id="org-example-001"} 0
mvgc_auth_failures_total{reason="invalid_key",org_id=""} 2

Metrics Reference

MetricTypeLabelsDescription
mvgc_auth_failures_totalCounterreason, org_idAuthentication failures before policy evaluation (missing_header, invalid_key, revoked, expired, suspended)
mvgc_auth_key_lookup_totalCounterresultAPI key cache lookup results (hit, miss, fallback)
mvgc_requests_totalCounteraction_type, decision, org_idTotal policy decisions by action type, decision, and org
mvgc_request_duration_msHistogramaction_type, decisionEnd-to-end request latency in milliseconds (buckets: 5, 10, 25, 50, 100, 250, 500, 1000)
mvgc_upstream_latency_secondsHistogramproviderTime spent waiting for the upstream provider response
mvgc_risk_scoreGaugeorg_idLast computed composite risk score per org (0.0--1.0)
mvgc_risk_signalsCountersignal_type, org_idRisk signals detected per type per org
mvgc_approvals_pendingGauge(none)Current number of open require_approval decisions awaiting a decision
mvgc_rate_limit_hitsCounterorg_idRequests rejected by the rate limiter per org
mvgc_quarantine_totalCounterorg_idWorkloads quarantined per org
mvgc_policy_deny_totalCounterorg_id, reason_codePolicy deny decisions per org and reason code
mvgc_org_overage_enforced_totalCounterorg_idRequests denied due to monthly overage per org
mvgc_pending_hash_submissionsGauge(none)Hash submissions pending delivery to the control plane
mvgc_config_sync_lag_secondsHistogram(none)Lag between config event timestamp and local processing

Dashboard API

The dashboard endpoints provide aggregated summaries without requiring a separate analytics stack. Both require the admin token.

# Summary: total requests, allow/deny counts for an org over a time window
curl -s "http://localhost:7080/v1/dashboard/summary?org_id=org-example-001&from=2026-03-03T00:00:00Z&to=2026-03-10T00:00:00Z" \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

# Decision breakdown: counts grouped by decision type
curl -s "http://localhost:7080/v1/dashboard/decisions?org_id=org-example-001" \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
# [{"decision":"allow","count":38},{"decision":"deny","count":4},{"decision":"rate_limit","count":1}]

GCP Cloud Monitoring (Infrastructure Alerts)

For GCP deployments, infrastructure-level alerts are provisioned at the infrastructure level and managed independently of the gateway application. These complement the Prometheus and SIEM layers above.

What fires an alert:

AlertConditionChannel
Cloud SQL memory> 80% for 5 minEmail + Google Chat (when configured)
Cloud SQL connections> 20 active for 5 minEmail + Google Chat
Cloud SQL disk> 80% for 5 minEmail + Google Chat
Cloud SQL CPU> 80% for 10 minEmail + Google Chat
CP gRPC unreachableTCP check failing from 2+ regionsEmail + Google Chat
Telemetry HTTPS down/healthz failing from 2+ regionsEmail + Google Chat
LB 5xx error rate> 5 errors/min for 5 minEmail + Google Chat
LB p99 latency> 2000ms for 5 minEmail + Google Chat

Google Chat webhook integration is configured at the infrastructure level. Contact your platform team to add or update webhook URLs.

Dashboard: GCP Console → Monitoring → Dashboards → Axemere Infrastructure shows Cloud SQL utilization, uptime check pass rates, LB request count and latency percentiles, and pod restart counts in a single view.

Responding to alerts: Each alert policy includes a documentation block with a runbook link. The most common responses:

  • Cloud SQL connections alert: check for connection pool leaks; add PgBouncer or upgrade the instance tier
  • Uptime check alert: check gateway health via /healthz; contact support if the gateway is unreachable
  • LB 5xx rate alert: correlate with Cloud SQL memory/connection alerts; often indicates DB unreachable
  • OOMKill alert: upgrade the gateway host instance tier; check for unbounded queries

SIEM Export

The gateway can stream events to external security systems. Configure one or both:

  • Webhook: set MVGC_EXPORT_WEBHOOK_URL (and optionally MVGC_EXPORT_WEBHOOK_TOKEN). Events are POST'd as JSON with retry logic (3 attempts, exponential backoff).
  • Syslog: set MVGC_EXPORT_SYSLOG_ADDR to tcp://host:port or udp://host:port.

Both can be configured simultaneously; events fan out to all configured exporters.

Exported event types: execution, approval, quarantine, risk_alert.

The risk_alert event is emitted whenever a request's composite risk score reaches 0.7 or higher. The event payload includes the risk score, active signals, and the request context (org, workload, caller). Use these events to trigger SIEM alerts for anomalous activity patterns without waiting for a quarantine policy decision.

Each execution event payload includes the full ExecutionRecord, which contains caller_ip, the source IP of the request as observed at the gateway. Use this field in your SIEM to correlate requests with network telemetry and identify anomalous source IPs.


Execution Record Fields

Every request processed by the gateway produces an execution record. This includes denied requests: every deny, rate_limit, and quarantine decision is recorded with full context (caller IP, workload, target host, reason). To identify services that need new routing rules, filter execution records by decision = 'deny' and inspect the action_type and target host fields. Key fields relevant to security investigation:

FieldDescription
record_idUnique UUIDv7 identifier for the record
org_idOrganization that owns the request
workload_idWorkload that submitted the request
caller_idCaller identifier within the workload (application-supplied)
caller_ipSource IP address, extracted server-side from the HTTP connection
connection_typeHow the request reached the gateway: direct_api, sdk_redirect, or connect_proxy; indexed for efficient filtering
action_typeAction type (e.g. ai.infer)
decisionPolicy decision: allow, deny, downgrade, require_approval, require_attribution, rate_limit, quarantine
event_timeRFC3339 UTC timestamp of the request
record_hashJCS SHA-256 hash of the canonical record, use with GET /v1/verify/{record_hash}
connector_versionVersion of the connector that executed the request
idempotency_keyClient-supplied idempotency key (if provided); same key returns cached result
tokens_inInput token count (separate from aggregate model_tokens_used)
tokens_outOutput token count (separate from aggregate model_tokens_used)

The full record JSON (including the Ed25519 signature envelope) is stored in record_json.

Ledger attribution context:

When the gateway submits record hashes to the control plane ledger (via SubmitRecordHashRequest), the request now carries full attribution context:

FieldDescription
credential_idThe credential used for this request (key alias credential ID)
project_idProject identifier from the request attribution
account_idAccount identifier from the request attribution

These fields flow into ledger entries, enabling audit queries by credential, project, or account in CP-connected mode.

Filtering records by source IP or connection type:

Use the Reports API or the console (Govern → Records) to filter execution records by caller_ip, connection_type, traffic_class, or any other field. See the Admin API Reference for query parameters.


Proxy Mode and Delegation Tokens

System proxy mode (connection_type: connect_proxy) and SDK redirect mode (connection_type: sdk_redirect) support passing a delegation token reference via the X-MVGC-Delegation-ID header. However, the full signed delegation token bytes cannot be transmitted in proxy mode -- the header carries only the token ID, not the signed payload.

Consequence: Delegation token scope constraints (actions_allow, targets_allow, budget limits) are not enforced for transparent proxy requests. The delegation_id is recorded in the execution record for audit purposes, but the gateway does not verify the token signature or check scope compliance.

For full delegation token enforcement -- including scope validation, budget constraints, and signature verification -- use explicit ActionRequest mode (POST /v1/actions:execute) with the delegation_token field.


SSL MITM Proxy

When MVGC_PROXY_MITM_ENABLED=true, the gateway intercepts HTTPS CONNECT tunnels to managed domains: it terminates TLS using a dynamically generated leaf certificate signed by the gateway's root CA, runs the decrypted request through the full policy pipeline (credential injection, budget enforcement, record writing), then re-encrypts the connection to the upstream provider. Traffic to non-managed domains passes through as an opaque TCP tunnel.

Environment variables

VariableRequiredDescription
MVGC_PROXY_MITM_ENABLEDyesSet to true to activate MITM mode. Default: false.
MVGC_MANAGED_DOMAINSyesComma-separated hostnames or *. wildcard prefixes to intercept, e.g. api.openai.com,*.anthropic.com.
MVGC_PROXY_CA_KEYnoAbsolute path to the root CA private key PEM. Default: <MVGC_KEY_DIR>/proxy-ca.key.
MVGC_PROXY_CA_CERTnoAbsolute path to the root CA certificate PEM. Default: <MVGC_KEY_DIR>/proxy-ca.crt.
MVGC_PROXY_CA_KEY_TYPEnoKey algorithm: ecdsa (P-256, default) or rsa (2048-bit).
MVGC_PROXY_LEAF_CERT_TTLnoLeaf certificate cache TTL, e.g. 24h. Default: 24h.

Example mvgc.yaml stanza:

proxy_enabled: true
mitm_enabled: true
managed_domains:
  - api.openai.com
  - "*.anthropic.com"
  - generativelanguage.googleapis.com

The root CA key and certificate are written to MVGC_KEY_DIR on first start (mode 0600 for the key, 0644 for the cert) and reloaded on subsequent starts. Rotating the root CA requires re-distributing the new certificate to all client trust stores.

Distributing the CA certificate

Clients must trust the gateway's root CA before HTTPS traffic will succeed through MITM mode. Retrieve the certificate from the gateway:

curl http://localhost:7080/v1/proxy/ca.crt > mvgc-proxy-ca.crt

This endpoint returns 404 when MVGC_PROXY_MITM_ENABLED is false.

Trust store installation by platform:

# macOS
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain mvgc-proxy-ca.crt

# Ubuntu / Debian
sudo cp mvgc-proxy-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

# RHEL / CentOS / Fedora
sudo cp mvgc-proxy-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

For tooling that manages its own trust bundle:

# curl (per-invocation)
curl --cacert mvgc-proxy-ca.crt --proxy http://localhost:7080 https://api.openai.com/...

# Python requests (per-process)
export REQUESTS_CA_BUNDLE=/path/to/mvgc-proxy-ca.crt
export SSL_CERT_FILE=/path/to/mvgc-proxy-ca.crt

Per-workload domain scope

The MVGC_MANAGED_DOMAINS environment variable sets the gateway-level default. Individual policy bundles can narrow or extend this list using the proxy: bundle key:

proxy:
  managed_domains:
    - api.openai.com
    - "*.anthropic.com"
  bypass_domains:
    - telemetry.anthropic.com   # always pass through

bypass_domains is evaluated first; a domain matching both lists is treated as bypassed. See proxy-mitm.yaml in the configuration reference for a complete bundle example.


Admin API Reference

The full endpoint listing -- credentials, workloads, policies, connectors, approvals, quarantine, nodes, rate limiting, metrics, and verification -- is in the Admin API Reference.

All endpoints require the MVGC-Admin-Token or Authorization: Bearer header (set via MVGC_ADMIN_TOKEN). Quick reference for the most common operations:

MethodEndpointUse
GET/v1/admin/policiesCheck active bundle
PUT/v1/admin/policiesHot-load a bundle
POST/v1/admin/policies/reloadReload from disk (picks up add-ons)
PUT/v1/admin/credentialsRegister a credential
PUT/v1/admin/workloadsRegister a workload
GET/v1/admin/approvalsList pending approvals
GET/v1/admin/quarantineList quarantine entries
GET/metricsPrometheus metrics
GET/healthzGateway health (no token required)