Policies

For: Platform operators and policy authors configuring policy bundles on Axemere Gateway.

Configuration Overview | Credentials | Workloads | Policies


Policies are organized into a bundle that controls evaluation order and expiry, and rule files that contain the actual governance rules.

Table of Contents

3.1 Bundle file

Location: configs/policies/bundle.yaml (or pushed via the admin API)

The bundle is the root of the policy configuration. It sets global defaults and controls which layers are evaluated, in what order.

Default bundle (skeleton + add-ons)

The default bundle.yaml shipped with the gateway is a skeleton -- it contains no rules of its own. Instead, rules are loaded automatically from the addons/ subdirectory under MVGC_POLICIES_DIR. This is the add-on system described in section 3.9.

schema: mvgc.policy_bundle.v1
bundle_id: default
version: "1.0.0"
issued_at: "2026-03-15T00:00:00Z"
expires_at: "2028-03-15T00:00:00Z"

defaults:
  decision: deny
  trace_level: matched_rules

evaluation:
  order:
    - identity
    - connectors
  stop_on:
    - deny
  merge_strategy: first_match

On startup, the gateway detects that the bundle has no inline_rules and no files: list. It then scans <MVGC_POLICIES_DIR>/addons/ and loads every .yaml file found there (symlinks are followed automatically). This means enabling or disabling a provider is as simple as placing or removing a file in addons/ and calling POST /v1/admin/policies/reload -- no restart required.

Advanced: file-referenced or inline bundles

For production governance with explicit per-layer rule files, use mvp_policy.yaml (see section 3.7) or write your own bundle. Three loading modes are supported:

# Mode 1: skeleton (default) -- loads add-ons from addons/ automatically
schema: mvgc.policy_bundle.v1
# ... no files: or inline_rules: ...

# Mode 2: file-referenced -- loads named rule files from MVGC_POLICIES_DIR
files:
  - targets.yaml
  - credential-rules.yaml
  - budgets.yaml
  - risk.yaml
  # transforms.yaml is optional; the binary embeds a default and uses it when absent

# Mode 3: inline -- embeds rules directly (used for admin API pushes and tests)
inline_rules:
  identity:
    - id: identity.allow.proxy
      priority: 100
      when:
        field: context.connection_type
        equals: connect_proxy
      effect:
        decision: allow

inline_rules takes precedence over files. Both take precedence over the add-on loader. A bundle with a non-empty files: or inline_rules: section never loads from addons/.

Bundle signature

signature:
  alg: ed25519
  kid: "kid_cp_policy_1"
  sig: "base64url..."

Populated by the CP when MVGC_BUNDLE_SIGN=true. See Signed Policy Bundles.

Bundle field reference

FieldTypeRequiredDescription
schemastringyesMust be mvgc.policy_bundle.v1.
bundle_idstringyesUnique bundle identifier.
versionstringyesVersion string (any format).
issued_atRFC3339noWhen the bundle was issued.
expires_atRFC3339noThe gateway rejects the bundle after this time.
defaults.decisionstringyesDecision applied when no rule matches. Typically deny. See decision valid values.
defaults.trace_levelstringnoHow much detail to include in policy traces. See defaults.trace_level valid values.
defaults.require_receiptboolnoWhen true, every execution must be recorded in the ledger.
evaluation.order[]stringyesLayer evaluation order.
evaluation.stop_on[]stringnoDecisions that halt evaluation early. See decision valid values.
evaluation.merge_strategystringnoHow multiple matching rules are combined. See evaluation.merge_strategy valid values.
signatureobjectnoEd25519 signature for bundle integrity. Verified when MVGC_BUNDLE_VERIFY_SIGNATURES=true.
files[]stringnoRelative paths to policy rule files (used with filesystem bundles).
inline_rulesmapnoRules embedded directly in the bundle. Keyed by layer name. Takes precedence over files.
proxy.managed_domains[]stringnoPer-bundle HTTPS domains to intercept (merged with gateway-level MVGC_MANAGED_DOMAINS).
proxy.bypass_domains[]stringnoDomains to explicitly pass through even if they match managed_domains.

defaults.trace_level valid values

ValueDescription
noneNo trace included in the execution record.
summaryFinal decision and matched layer names only.
matched_rulesIncludes the IDs of all rules that matched.
fullFull evaluation detail including all context fields and rule outcomes.

evaluation.merge_strategy valid values

ValueDescription
first_matchStop at the first rule that matches within a layer. Recommended.
most_specificApply the most specific matching rule (by priority).
priorityEvaluate all rules; apply the one with the lowest priority number.

3.2 Rule files

Each policy layer has its own YAML file. Rules in each file share the same schema and layer tag.

Rule file structure

schema: mvgc.policy_rules.v1
layer: identity
rules:
  - id: identity.allow.all.explicit
    description: "Allow all explicit action requests"
    priority: 100
    when:
      field: context.connection_type
      equals: direct_api
    effect:
      decision: allow
      reason: "explicit action request allowed"

Rule file header fields

FieldTypeDescription
schemastringMust be mvgc.policy_rules.v1 or mvgc.rule_file.v1.
layerstringThe evaluation layer this file belongs to.
rules[]RuleThe list of policy rules.

3.3 Rule fields

FieldTypeRequiredDescription
idstringyesUnique rule identifier. Included in policy traces. Convention: <layer>.<effect>.<target>.
descriptionstringnoHuman-readable explanation of the rule's intent.
priorityintyesLower numbers are evaluated first within a layer. Ties are resolved by file order.
whenConditionnoMatch expression. When omitted, the rule always matches.
effectEffectyesThe outcome when the rule matches.

3.4 Condition language

Conditions are YAML objects that evaluate fields from the request context.

Logical combinators

# All sub-conditions must match
when:
  all:
    - field: context.connection_type
      equals: direct_api
    - field: context.attribution.project_id
      exists: true

# Any sub-condition must match
when:
  any:
    - field: action.target_host
      equals: api.openai.com
    - field: action.target_host
      equals: api.anthropic.com

Combinators can be nested to any depth.

Leaf operators

OperatorTypeDescriptionExample
equalsstringExact string matchequals: api.openai.com
in[]stringValue is in the listin: ["api.openai.com", "api.anthropic.com"]
not_in[]stringValue is not in the listnot_in: ["api.evil.com"]
existsboolField is present and non-emptyexists: true
regexstringRE2-compatible regex matchregex: "^proj-[0-9]+"
prefixstringString starts with valueprefix: "proj-"
suffixstringString ends with valuesuffix: ".openai.azure.com"
ltdecimal stringNumeric less-thanlt: "1.00"
ltedecimal stringNumeric less-than-or-equallte: "0.00"
gtdecimal stringNumeric greater-thangt: "2.00"
gtedecimal stringNumeric greater-than-or-equalgte: "0.50"

Numeric operators accept decimal strings to avoid floating-point imprecision. Use quoted values like "1.00", "0.75".

Context field reference

Fields are addressed as dotted paths. All paths under context.* or action.* are valid.

Identity and ingress:

FieldDescription
context.org_idOrganization ID of the caller
context.workload_idWorkload ID of the calling service
context.connection_typedirect_api, sdk_redirect, or connect_proxy
context.delegation_idDelegation token ID, if present
context.principal_idPrincipal identity

Action:

FieldDescription
context.action.typeAction type: ai.infer, http.request, etc.
context.action.methodHTTP method: GET, POST, etc.
context.action.target_hostTarget hostname (e.g. api.openai.com)
context.action.target_pathTarget URL path
context.action.params.<key>Action parameter by key (e.g. context.action.params.model)
context.action.headers_presentComma-separated list of present header names
context.action.header.<name>Value of a specific request header
action.target_hostShorthand for context.action.target_host (usable in inline rules)

Attribution:

FieldDescription
context.attribution.customer_idCustomer identifier
context.attribution.account_idBilling account identifier
context.attribution.project_idProject identifier
context.attribution.labels.<key>Label value by key

Cost estimates:

FieldDescription
context.estimates.tokens_inEstimated input tokens
context.estimates.tokens_out_maxEstimated maximum output tokens
context.estimates.cost_usd_maxEstimated maximum cost as decimal string

Budget state:

FieldDescription
context.budgets.org_usd_remainingRemaining org budget as decimal string
context.budgets.project_usd_remainingRemaining project budget as decimal string

Risk signals:

FieldDescription
context.risk.scoreComposite risk score (0.0 to 1.0)
context.risk.request_rateRecent request rate (requests/minute)
context.risk.cost_rateRecent cost rate (USD/minute, decimal string)
context.risk.signalsActive signal names (e.g. rate_spike, cost_anomaly)

Delegation:

FieldDescription
context.delegation_idToken ID from the delegation token
delegation.actions_allowActions allowed by the delegation scope
delegation.targets_allowTargets allowed by the delegation scope

Connector candidates:

FieldDescription
context.candidates.connectorsList of eligible connector IDs
context.candidates.credentialsList of eligible credential IDs

3.5 Effect language

The effect block defines what happens when a rule matches. Effects can produce a decision, modify the request, or select infrastructure.

decision valid values

ValueHTTP StatusDescription
allow200Proceed with the request.
deny403Reject the request.
downgrade200Allow but mutate the request (e.g. swap model). Use with mutate.
require_approval202Pause and create an approval request. Returns approval_id.
rate_limit429Throttle the caller. Returns Retry-After header. Use with rate_limit.
quarantine403Reject and create a quarantine record for review. Use with quarantine.
require_attribution403Reject with an attribution-specific reason.

Credential selection

Credential IDs referenced here must be registered -- see Credentials.

effect:
  # Select by ID (preferred):
  select_credential: cred-openai

  # Or with explicit struct:
  select_credential:
    credential_id: cred-openai

  # Or select by attributes (policy engine picks the first match):
  select_credential:
    provider: openai
    mode: alias
    billing_owner: customer
FieldTypeDescription
credential_idstringExact credential ID to use.
providerstringFilter by provider name.
modestringFilter by credential mode. See Credentials -- mode valid values.
billing_ownerstringFilter by billing owner. See Credentials -- billing_owner valid values.

Connector selection

effect:
  select_connector:
    connector_id: openai

Valid connector IDs match the provider values in the credentials provider table: openai, anthropic, gemini, azure_openai, cohere, generic_http.

Attribution enforcement

effect:
  enforce_attribution:
    require:
      - project_id
      - account_id
    allow_overrides:
      - project_id
      - labels
FieldTypeDescription
require[]stringAttribution fields that must be present. Request is denied if any are missing. Valid values: customer_id, account_id, project_id, labels.
allow_overrides[]stringAttribution fields the caller may override.

Default attribution

effect:
  default_attribution:
    customer_id: cust-default
    account_id: acct-default
    project_id: proj-default
    labels:
      env: prod

Applies before enforce_attribution. Fields are only set if not already present in the request.

Budget enforcement

effect:
  enforce_budget:
    limit_key: "project:proj-123"
    max_usd_per_day: "200.00"
    max_usd_per_request: "2.00"
FieldTypeDescription
limit_keystringBudget scope key (e.g. project:proj-123, org:org-001).
max_usd_per_daydecimal stringDaily spend cap.
max_usd_per_requestdecimal stringPer-request cost cap.

Request mutation

Use mutate with decision: downgrade to rewrite action parameters:

effect:
  decision: downgrade
  mutate:
    action.params.model: "gpt-4o-mini"
    action.headers.X-Cost-Tier: "economy"
    action.target_path: "/v1/chat/completions"
    action.method: "POST"
  reason: "model downgraded due to cost threshold"

Valid mutate key prefixes:

PrefixDescription
action.params.<key>Set an action parameter
action.headers.<name>Set a request header
action.target_hostOverride the target hostname
action.target_pathOverride the target path
action.methodOverride the HTTP method

Rate limiting

effect:
  decision: rate_limit
  rate_limit:
    key: "org"
    limit: 100
    window: "1m"
FieldTypeDescription
keystringRate limit bucket key. "org" uses the org ID; any string is allowed.
limitnumberMaximum requests per window.
windowdurationTime window (e.g. "1m", "5m", "1h").

Quarantine

effect:
  decision: quarantine
  quarantine:
    reason: "critical risk score exceeded threshold"
FieldTypeDescription
reasonstringHuman-readable reason stored in the quarantine record.

Require receipt

effect:
  require_receipt: true

When true, overrides the bundle default and requires this execution to be recorded in the ledger.


3.6 Per-layer rule files

Default configuration uses add-ons. The per-layer files described in this section are used with mvp_policy.yaml and the files: loading mode for production governance. The out-of-the-box bundle.yaml loads rules from the addons/ directory instead. See section 3.9 for the add-on system.

The mvp_policy.yaml bundle evaluates five rule-file layers. Identity filtering and delegation token checks are handled by gateway-level configuration settings (see allowed_connection_types and the connector settings below) rather than policy rules.

targets
host allowlist

credentials
select key + enforce attribution

budgets
cost caps

risk
anomaly detection

transforms
header injection + mutations

PolicyDecision -> Connector execution

Rules within a layer are evaluated in ascending priority order; the lowest priority number runs first. With merge_strategy: first_match, evaluation stops at the first matching rule in each layer.

Gateway-level enforcement settings

Two behaviours that were formerly expressed as policy rules are now controlled by gateway config settings, keeping the policy files focused on per-request governance logic:

SettingConfig keyEnv varDefaultDescription
Allowed connection typesgateway.allowed_connection_typesMVGC_ALLOWED_CONNECTION_TYPES(all)Comma-separated list of permitted connection types (direct_api, sdk_redirect, connect_proxy). Empty = all permitted.
Allow generic HTTP connectorconnectors.allow_generic_httpMVGC_ALLOW_GENERIC_HTTPtrueWhen false, the generic HTTP fallback connector is disabled; only named AI-provider connectors are available.
# configs/mvgc.yaml
gateway:
  allowed_connection_types:
    - direct_api
    - sdk_redirect
    - connect_proxy   # remove to disable HTTPS proxy interception gateway-wide

connectors:
  allow_generic_http: true     # set to false to restrict to AI providers only

3.6.1 targets.yaml

Path: configs/policies/targets.yaml

Controls which upstream hostnames the gateway will forward requests to. This is the primary safeguard against requests being routed to unauthorized external services. Rules match on context.action.target_host, which is the hostname extracted from the request (explicit action requests use action.target_host; proxy mode uses the HTTP Host header).

Default rules:

schema: mvgc.policy_rules.v1
layer: targets
rules:
  - id: targets.allow.openai
    priority: 100
    when:
      all:
        - field: context.action.target_host
          in: ["api.openai.com"]
    effect:
      decision: allow
      reason: "openai target allowed"

  - id: targets.allow.anthropic
    priority: 100
    when:
      all:
        - field: context.action.target_host
          in: ["api.anthropic.com"]
    effect:
      decision: allow
      reason: "anthropic target allowed"

  - id: targets.allow.gemini
    priority: 100
    when:
      all:
        - field: context.action.target_host
          in: ["generativelanguage.googleapis.com"]
    effect:
      decision: allow
      reason: "gemini target allowed"

  - id: targets.allow.azure_openai
    priority: 100
    when:
      any:
        - field: context.action.target_host
          suffix: "openai.azure.com"
        - field: context.action.target_host
          suffix: "cognitiveservices.azure.com"
    effect:
      decision: allow
      reason: "azure openai target allowed"

  - id: targets.deny.unknown
    priority: 1000
    when:
      all:
        - field: context.action.target_host
          not_in: ["api.openai.com", "api.anthropic.com", "generativelanguage.googleapis.com"]
    effect:
      decision: deny
      reason: "target host not in allowlist"

Note on the deny rule: targets.deny.unknown uses a not_in list that intentionally omits Azure hostnames -- those are caught by targets.allow.azure_openai at priority 100 before the deny rule at priority 1000 is reached. If you add providers, add both an allow rule for them and include their hostnames in the not_in list of the deny rule (or replace the catch-all approach entirely with a single deny rule at the bottom that matches everything).

Common customizations:

# Add a private or on-premise LLM endpoint:
- id: targets.allow.private-llm
  priority: 100
  when:
    field: context.action.target_host
    equals: llm.internal.corp.example
  effect:
    decision: allow
    reason: "internal LLM endpoint allowed"

# Restrict a workload to a single provider:
- id: targets.deny.non-openai.billing-app
  priority: 150
  when:
    all:
      - field: context.workload_id
        equals: wl-billing-app
      - field: context.action.target_host
        not_in: ["api.openai.com"]
  effect:
    decision: deny
    reason: "billing-app may only reach OpenAI"

3.6.2 credential-rules.yaml

Path: configs/policies/credential-rules.yaml

Selects which credential to use for each request and enforces attribution requirements. Credential selection rules match on the target host (and optionally on workload, ingress mode, or attribution fields) and set both select_connector and select_credential together. The catch-all deny at the bottom rejects any request that does not match a credential rule -- typically because project_id is missing.

Default rules:

schema: mvgc.policy_rules.v1
layer: credentials
rules:
  - id: creds.select.openai.prod
    priority: 100
    when:
      all:
        - field: context.action.target_host
          equals: api.openai.com
        - field: context.attribution.project_id
          exists: true
    effect:
      select_connector:
        connector_id: openai
      select_credential:
        credential_id: cred-openai
        provider: openai
        mode: alias
        billing_owner: customer
      enforce_attribution:
        require:
          - project_id
      decision: allow
      reason: "openai credential selected"

  # Identical pattern for creds.select.anthropic.prod, creds.select.gemini.prod,
  # creds.select.azure_openai.prod -- each matches on target_host + project_id
  # and selects the corresponding credential.

  - id: creds.deny.missing.project
    priority: 500
    when:
      all:
        - field: context.attribution.project_id
          exists: false
    effect:
      decision: deny
      reason: "project_id is required for attribution"

Every credential selection rule requires project_id to be present in the attribution block. The select_connector and select_credential effects work together: select_connector routes to the provider-specific connector, select_credential tells the connector which API key to use.

Common customizations:

# Add a second OpenAI credential for a different team:
- id: creds.select.openai.team-b
  priority: 90
  when:
    all:
      - field: context.action.target_host
        equals: api.openai.com
      - field: context.workload_id
        prefix: "wl-team-b-"
      - field: context.attribution.project_id
        exists: true
  effect:
    select_connector:
      connector_id: openai
    select_credential:
      credential_id: cred-openai-team-b
    enforce_attribution:
      require:
        - project_id
    decision: allow

# Apply default attribution before the attribution check:
- id: creds.default.attribution
  priority: 60
  effect:
    default_attribution:
      project_id: proj-unattributed

3.6.3 budgets.yaml

Path: configs/policies/budgets.yaml

Enforces cost caps at both the per-request and per-project-per-day levels. Cost estimates are computed by the gateway from the request body before this layer runs and are available as context.estimates.cost_usd_max. Daily spend state is tracked in Postgres and available as context.budgets.project_usd_remaining.

Default rules:

schema: mvgc.policy_rules.v1
layer: budgets
rules:
  # Hard-deny if a single request's estimated cost exceeds $2.00.
  - id: budgets.deny.request.cap
    priority: 100
    when:
      all:
        - field: context.estimates.cost_usd_max
          gt: "2.00"
    effect:
      decision: deny
      reason: "per-request cost cap exceeded (max $2.00)"

  # Hard-deny if the project has exhausted its daily budget.
  - id: budgets.deny.project.daily.exhausted
    priority: 50
    when:
      all:
        - field: context.budgets.project_usd_remaining
          lte: "0.00"
    effect:
      decision: deny
      reason: "project daily budget exhausted"

  # Downgrade the model when estimated cost is high but under the hard cap.
  - id: budgets.downgrade.high.cost
    priority: 200
    when:
      all:
        - field: context.action.type
          equals: ai.infer
        - field: context.estimates.cost_usd_max
          gt: "1.00"
    effect:
      decision: downgrade
      mutate:
        action.params.model: "gpt-4o-mini"
      reason: "cost estimate exceeds threshold; downgrading model"

Rule priority determines the order: exhausted daily budget (priority 50) is caught before the per-request cap (100), which is caught before the downgrade (200). A request with an estimated cost over $2 is denied outright; one between $1-$2 has its model swapped to gpt-4o-mini instead.

Estimates vs actuals: context.estimates.cost_usd_max is a pre-execution estimate based on input token count and model pricing. It may be conservative. context.budgets.project_usd_remaining reflects actual committed spend from previous requests. Use both together for effective budget governance.


3.6.4 risk.yaml

Axemere Console — Policies (Risk Scoring)

Path: configs/policies/risk.yaml

Applies automated governance based on the composite risk score and real-time signal data computed by the gateway's risk scorer before policy evaluation. The risk scorer populates context.risk.score (0.0--1.0) and context.risk.signals using a sliding time window.

Risk score signals:

SignalWeightTriggers when
rate_spike0.40Requests in the last 1m > 3× the baseline avg req/min (over 5m window). Requires ≥5 baseline observations.
cost_anomaly0.30USD spend in last 1m > 3× the baseline avg USD/min. Same minimum baseline guard.
target_diversity0.15Distinct target hosts in last 1m > 3× the baseline rate from the 1m–5m prior window (the recent window is excluded from baseline to avoid it always subsuming itself). Requires ≥3 recent hosts.
time_of_day0.15Current time falls outside MVGC_RISK_BUSINESS_HOURS (format: HH:MM-HH:MM,TZ). Only fires if the env var is set.

Signals require at least five observations in the baseline window before activating. A newly started gateway or a workload with very few requests will not trigger risk rules even if the score appears elevated.

Default rules:

schema: mvgc.policy_rules.v1
layer: risk
rules:
  # Application-tagged high-risk requests require approval.
  - 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"

  # Automated quarantine for critical risk scores.
  - id: risk.quarantine.critical
    priority: 10
    when:
      field: context.risk.score
      gt: "0.8000"
    effect:
      decision: quarantine
      quarantine:
        reason: "automated quarantine: risk score exceeds critical threshold"

  # Require approval for elevated but non-critical risk scores.
  - id: risk.approval.elevated
    priority: 20
    when:
      field: context.risk.score
      gt: "0.5000"
    effect:
      decision: require_approval
      reason: "elevated risk score requires approval"

  # Rate limit callers with a request rate spike.
  - id: risk.ratelimit.burst
    priority: 30
    when:
      field: context.risk.request_rate
      gt: "100.00"
    effect:
      decision: rate_limit
      rate_limit:
        key: "org"
        limit: 100
        window: "1m"

Rules fire in priority order: critical quarantine (10) -> approval for elevated (20) -> rate limit burst (30) -> application-supplied risk_tier tag (100). Because the bundle's stop_on includes quarantine and rate_limit, hitting any of the first three rules stops further layer evaluation immediately.

Common customizations:

# Lower the quarantine threshold for a stricter posture:
- id: risk.quarantine.strict
  priority: 10
  when:
    field: context.risk.score
    gt: "0.6000"
  effect:
    decision: quarantine
    quarantine:
      reason: "strict mode: quarantine threshold 0.60"

# Require approval specifically for off-hours requests:
- id: risk.approval.off-hours
  priority: 25
  when:
    field: context.risk.signals
    in: ["time_of_day"]
  effect:
    decision: require_approval
    reason: "off-hours request requires approval"

3.6.5 transforms.yaml

Path: configs/examples/transforms.yaml

The last layer evaluated before connector execution. Used for non-security mutations to the outgoing request: injecting headers, normalizing paths, or swapping models. Because this layer only runs after all deny/allow/rate-limit decisions have already been made, mutations here are guaranteed to apply only to requests that have fully passed governance.

Default rules:

schema: mvgc.policy_rules.v1
layer: transforms
rules:
  # Inject an Axemere tracking header on every forwarded request.
  - id: transforms.header.request_id
    priority: 100
    effect:
      mutate:
        "action.headers.X-MVGC-Request-ID": "injected"

  # Supply a default path when the caller omitted one.
  - id: transforms.path.api_version
    priority: 200
    when:
      field: context.action.target_path
      exists: false
    effect:
      mutate:
        "action.target_path": "/v1/"

The X-MVGC-Request-ID rule has no when clause and therefore matches every request. The path normalization rule only fires when target_path is absent.

mutate without decision: When a transform rule omits the decision field, it applies the mutation while leaving the existing decision unchanged. This is the correct pattern for header injection and path normalization. Use decision: downgrade only when the mutation itself represents a policy downgrade (e.g., swapping a premium model to an economy model).

Common customizations:

# Inject attribution as a header for downstream audit:
- id: transforms.header.project
  priority: 100
  when:
    field: context.attribution.project_id
    exists: true
  effect:
    mutate:
      "action.headers.X-Project-ID": "{{context.attribution.project_id}}"

# Normalize gpt-4 to the current recommended version:
- id: transforms.model.gpt4.normalize
  priority: 150
  when:
    field: context.action.params.model
    equals: "gpt-4"
  effect:
    mutate:
      "action.params.model": "gpt-4o"

3.6.6 Access Rules (console)

The hosted console's Policies → Access Rules tab exposes four organization-wide toggles that wrap common credential and proxy behaviors without hand-editing YAML:

ToggleEffect
AWS Bedrock — CredentialStores the org AWS Bedrock credential (SigV4 access/secret key pair, or a Bedrock API key). Required before enabling provider-bedrock.
Claude CLI ProxyAllows Claude CLI companion traffic (claude.ai, statsig.anthropic.com, sentry.io) through a transparent MITM proxy; the console equivalent of the proxy: key described in proxy-mitm.yaml.
Deny Unknown HostsBlocks requests to any host not explicitly permitted by policy. Recommended for production; equivalent to the proxy.deny.unknown-host rule in proxy-credentials.yaml.
Google Vertex AI — CredentialStores the org Google Vertex AI service-account credential. Required before enabling provider-vertex.

The two credential toggles (AWS Bedrock, Google Vertex AI) stay disabled until the corresponding credential is configured; enabling them makes that provider selectable elsewhere in the console (Providers, Credentials).

Axemere Console — Policies (Access Rules)


3.7 Additional bundle files

Beyond the add-on system and the eight per-layer rule files, the configs/policies/ directory ships two standalone complete bundles. These are not loaded automatically -- they are templates and reference configurations intended to be pushed via the admin API.

mvp_policy.yaml

Path: configs/examples/mvp_policy.yaml

The full production-ready bundle. Unlike bundle.yaml (which loads add-ons from addons/ and evaluates the connectors layer only), mvp_policy.yaml loads named rule files for the targets, credentials, budgets, risk, and transforms layers and configures a strict stop_on list.

schema: mvgc.policy_bundle.v1
bundle_id: "bundle-mvp-001"
version: "2026.03.06.1"
issued_at: "2026-03-06T00:00:00Z"
expires_at: "2027-03-06T00:00:00Z"

defaults:
  decision: deny
  require_receipt: false
  trace_level: matched_rules

evaluation:
  order:
    - targets
    - credentials
    - budgets
    - risk
    - transforms
  stop_on:
    - deny
    - require_approval
    - require_attribution
    - rate_limit
    - quarantine
  merge_strategy: first_match

files:
  - targets.yaml
  - credential-rules.yaml
  - budgets.yaml
  - risk.yaml
  # transforms.yaml is optional; the binary embeds a default and uses it when absent

Comparison with bundle.yaml (the default startup bundle):

bundle.yamlmvp_policy.yaml
Rules locationAdd-ons loaded from addons/External files via files:
Layers evaluatedconnectors (from add-ons)targets, credentials, budgets, risk, transforms
Attribution enforcementNonerequire_attribution in stop_on
stop_ondeny onlydeny, require_approval, require_attribution, rate_limit, quarantine
Intended useQuick-start, proxy modeFull production governance

To activate mvp_policy.yaml:

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

After pushing, edit the individual layer files (targets.yaml, credential-rules.yaml, etc.) and push the bundle again to pick up changes. The file paths in files: are resolved relative to MVGC_POLICIES_DIR.


proxy-credentials.yaml

Path: configs/examples/proxy-credentials.yaml

A minimal single-layer bundle that maps AI provider hostnames to their credentials. It evaluates only the connectors layer and is intended as a customization template for transparent proxy credential routing.

schema: mvgc.policy_bundle.v1
bundle_id: proxy-credentials
version: "1.0.0"
...
evaluation:
  order:
    - connectors
  stop_on:
    - deny
  merge_strategy: first_match

inline_rules:
  connectors:
    - id: proxy.allow.openai
      when:
        field: action.target_host
        equals: api.openai.com
      effect:
        decision: allow
        select_credential: cred-openai

    - id: proxy.allow.anthropic
      when:
        field: action.target_host
        equals: api.anthropic.com
      effect:
        decision: allow
        select_credential: cred-anthropic

    # ... gemini, azure_openai, cohere ...

    - 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"

This bundle is not loaded on startup and does not include identity, targets, budgets, risk, or transforms layers -- it performs only hostname-to-credential routing. Use it as a starting point when you need to:

  • Override which credential maps to a specific provider
  • Add custom Azure deployment hostnames (<resource>.openai.azure.com)
  • Add non-standard or private LLM endpoints
  • Restrict which hosts the transparent proxy will forward to

Push a customized copy to replace the active bundle:

cp configs/examples/proxy-credentials.yaml my-proxy-config.yaml
# Edit my-proxy-config.yaml to add or change host mappings, then:
curl -X PUT http://localhost:7080/v1/admin/policies \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/yaml" \
  --data-binary @my-proxy-config.yaml

proxy-mitm.yaml

Path: configs/examples/proxy-mitm.yaml

An example bundle showing the proxy: key, which controls per-workload SSL MITM domain scope when MVGC_PROXY_MITM_ENABLED=true. This is the only bundle file that uses the proxy: top-level key; it is otherwise a normal bundle and may include any policy rules.

schema: mvgc.policy_bundle.v1
bundle_id: proxy-mitm-example
version: "1.0.0"
issued_at: "2026-03-19T00:00:00Z"
expires_at: "2027-03-19T00:00:00Z"

# Per-bundle MITM domain scope.
# managed_domains: intercept these hosts (merged with MVGC_MANAGED_DOMAINS).
# bypass_domains:  always pass through even if they match managed_domains.
proxy:
  managed_domains:
    - api.openai.com
    - "*.anthropic.com"
    - generativelanguage.googleapis.com
  bypass_domains:
    - telemetry.anthropic.com

defaults:
  decision: deny

evaluation:
  order:
    - identity
    - connectors
    - targets
  stop_on:
    - deny
  merge_strategy: first_match

The proxy: key is validated at bundle load time. Empty strings in either list are rejected. The bypass_domains list is checked first; a domain that matches both lists is treated as bypassed.


When to use each bundle:

ScenarioApproach
Getting started, transparent proxy with env var credentialsbundle.yaml (default, loads add-ons from addons/)
Production governance with all eight policy layersPush mvp_policy.yaml via admin API
Customising only which credentials map to which hostsproxy-credentials.yaml as a template
Configuring per-workload MITM domain scopeproxy-mitm.yaml as a template
Claude Code CLI proxymvgc-gateway addon enable claude-cli-proxy (see section 3.9)
Completely custom rulesWrite your own bundle and push via admin API

3.8 Complete examples

Allow all explicit requests, select credential by target host

# policies/identity.yaml
schema: mvgc.policy_rules.v1
layer: identity
rules:
  - id: identity.allow.explicit
    priority: 100
    when:
      field: context.connection_type
      equals: direct_api
    effect:
      decision: allow
# policies/credential-rules.yaml
schema: mvgc.policy_rules.v1
layer: credentials
rules:
  - id: creds.select.openai
    priority: 100
    when:
      all:
        - field: context.action.target_host
          equals: api.openai.com
        - field: context.attribution.project_id
          exists: true
    effect:
      select_connector:
        connector_id: openai
      select_credential: cred-openai
      enforce_attribution:
        require:
          - project_id
      decision: allow
      reason: "openai credential selected"

  - id: creds.deny.missing.project
    priority: 500
    when:
      field: context.attribution.project_id
      exists: false
    effect:
      decision: deny
      reason: "project_id is required"

Budget enforcement with model downgrade

# policies/budgets.yaml
schema: mvgc.policy_rules.v1
layer: budgets
rules:
  - id: budgets.deny.request.cap
    priority: 100
    when:
      field: context.estimates.cost_usd_max
      gt: "2.00"
    effect:
      decision: deny
      reason: "per-request cost cap exceeded ($2.00 max)"

  - id: budgets.downgrade.high.cost
    priority: 200
    when:
      all:
        - field: context.action.type
          equals: ai.infer
        - field: context.estimates.cost_usd_max
          gt: "1.00"
    effect:
      decision: downgrade
      mutate:
        action.params.model: "gpt-4o-mini"
      reason: "cost estimate exceeds threshold; downgrading model"

Risk-based quarantine and rate limiting

# policies/risk.yaml
schema: mvgc.policy_rules.v1
layer: risk
rules:
  - id: risk.quarantine.critical
    priority: 10
    when:
      field: context.risk.score
      gt: "0.8000"
    effect:
      decision: quarantine
      quarantine:
        reason: "automated quarantine: risk score exceeds critical threshold"

  - id: risk.approval.elevated
    priority: 20
    when:
      field: context.risk.score
      gt: "0.5000"
    effect:
      decision: require_approval
      reason: "elevated risk score requires approval"

  - id: risk.ratelimit.burst
    priority: 30
    when:
      field: context.risk.request_rate
      gt: "100.00"
    effect:
      decision: rate_limit
      rate_limit:
        key: "org"
        limit: 100
        window: "1m"

Inline bundle (useful for the admin push API)

schema: mvgc.policy_bundle.v1
bundle_id: my-custom-bundle
version: "1.0.0"
issued_at: "2026-03-15T00:00:00Z"
expires_at: "2027-03-15T00:00:00Z"

defaults:
  decision: deny
  trace_level: matched_rules

evaluation:
  order:
    - identity
    - connectors
  stop_on:
    - deny
  merge_strategy: first_match

inline_rules:
  identity:
    - id: identity.allow.all
      priority: 100
      effect:
        decision: allow

  connectors:
    - id: connectors.select.openai
      priority: 100
      when:
        field: action.target_host
        equals: api.openai.com
      effect:
        decision: allow
        select_credential: cred-openai

3.9 Policy Add-Ons

Axemere Console — Policies (Advanced)

The add-on system gives you a Unix-style drop-in mechanism for policy rules. Every rule file in the available/ catalog is inactive by default; placing a symlink (or copy) in addons/ activates it. The gateway loads all .yaml files from addons/ on startup, following symlinks automatically.

<MVGC_POLICIES_DIR>/
├── bundle.yaml              <- skeleton: evaluation config, no inline rules
├── available/               <- catalog of all rule files (always present, inactive)
│   ├── core-identity.yaml
│   ├── provider-openai.yaml
│   ├── provider-anthropic.yaml
│   ├── provider-gemini.yaml
│   ├── provider-azure-openai.yaml
│   ├── provider-cohere.yaml
│   ├── deny-unknown-host.yaml
│   └── claude-cli-proxy.yaml
└── addons/                  <- active add-ons (symlinks or copies from available/)
    ├── core-identity.yaml          -> ../available/core-identity.yaml
    ├── provider-openai.yaml        -> ../available/provider-openai.yaml
    ├── provider-anthropic.yaml     -> ../available/provider-anthropic.yaml
    ├── provider-gemini.yaml        -> ../available/provider-gemini.yaml
    ├── provider-azure-openai.yaml  -> ../available/provider-azure-openai.yaml
    ├── provider-cohere.yaml        -> ../available/provider-cohere.yaml
    └── deny-unknown-host.yaml      -> ../available/deny-unknown-host.yaml

claude-cli-proxy.yaml is in the catalog but not enabled by default. Users opt in.

Add-on catalog

Add-onLayerWhat it doesEnabled by default
core-identityidentityAllows all three connection types (direct_api, sdk_redirect, connect_proxy)Yes
provider-openaiconnectorsRoutes api.openai.com -> cred-openaiYes
provider-anthropicconnectorsRoutes api.anthropic.com -> cred-anthropicYes
provider-geminiconnectorsRoutes generativelanguage.googleapis.com -> cred-geminiYes
provider-azure-openaiconnectorsRoutes *.openai.azure.com -> cred-azure-openaiYes
provider-cohereconnectorsRoutes api.cohere.com -> cred-cohereYes
deny-unknown-hostconnectorsCatch-all deny (priority 500) for any unmatched target hostYes
claude-cli-proxyconnectorsPassthrough rules for claude.ai, statsig.anthropic.com, sentry.io -- used when proxying Claude Code CLINo

Enabling and disabling add-ons

Use the mvgc-gateway addon subcommand to manage add-ons:

# List all available add-ons and their enabled/disabled status
mvgc-gateway addon list

# Enable the Claude CLI proxy add-on
mvgc-gateway addon enable claude-cli-proxy

# Disable an add-on
mvgc-gateway addon disable provider-cohere

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

The addon enable command creates a symlink in addons/ pointing to available/<name>.yaml. The addon disable command removes the symlink. Call POST /v1/admin/policies/reload to pick up the change immediately -- no restart needed.

CP-connected gateways: syncing addon state from the control plane

When your gateway is connected to a control plane, the org admin enables and disables add-ons from the console. Use addon sync to pull that state down to the local filesystem so it survives offline periods:

# Reconcile local addon symlinks with the CP's enabled state for this org
MVGC_CP_ADDR=cp.example.com:9090 \
MVGC_ORG_ID=org-abc123 \
MVGC_CP_TOKEN=$MVGC_CP_TOKEN \
mvgc-gateway addon sync

addon sync calls ListAddons on the CP and then enables or disables local symlinks to match. Add-ons that are enabled in the CP but whose YAML is not installed locally are skipped with a warning (upgrade the gateway package to get the latest catalog).

Env varRequiredDescription
MVGC_CP_ADDRYesControl plane gRPC address (e.g. cp.example.com:9090)
MVGC_ORG_IDYesOrg ID whose addon state to sync
MVGC_CP_TOKENNoBearer token for CP authentication
MVGC_CP_CA_CERTNoPath to CA cert PEM for CP TLS verification
MVGC_CP_SKIP_TLS_VERIFYNoSet true to skip TLS cert verification (dev only)
MVGC_CP_NO_TLSNoSet true to disable TLS entirely (local/test only)

The command is idempotent: safe to run on every deploy or from a cron job.

MVGC_POLICIES_DIR controls where the gateway looks for both available/ and addons/. The default is configs/policies/ relative to the working directory; installed packages set it to the installation-specific path.

Claude Code CLI proxy example:

Claude CLI contacts four hosts; only one is an AI API endpoint:

HostTraffic typeGateway handling
api.anthropic.comAI API callsGoverned -- Anthropic connector, credential injection, metering
claude.aiAuth / webPassthrough -- original headers forwarded unchanged
statsig.anthropic.comFeature flags / telemetryPassthrough
sentry.ioError reportingPassthrough
# Enable the add-on and reload (no restart needed)
mvgc-gateway addon enable claude-cli-proxy
curl -s -X POST http://localhost:7080/v1/admin/policies/reload \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .

# Set your Anthropic key and point Claude CLI at the gateway
export ANTHROPIC_API_KEY=sk-ant-...
export HTTPS_PROXY=http://localhost:7080
claude "Hello"

If the gateway terminates TLS with its own certificate, also set:

export NODE_EXTRA_CA_CERTS=/path/to/mvgc-ca.crt

How passthrough works: A rule with decision: allow and no select_credential causes the gateway to forward the request through the generic HTTP connector with all original client headers intact. No API key injection occurs.

How to create your own add-on

An add-on is a YAML rule file with the mvgc.rule_file.v1 schema and a single layer tag. It follows the same format as any other rule file (see section 3.2).

Step 1 -- Write the rule file

Place it in <MVGC_POLICIES_DIR>/available/. The filename (without .yaml) becomes the add-on name.

# available/provider-my-llm.yaml
schema: mvgc.rule_file.v1
layer: connectors
rules:
  - id: proxy.allow.my-llm
    priority: 100
    when:
      field: action.target_host
      equals: api.my-llm-provider.example
    effect:
      decision: allow
      select_credential: cred-my-llm
      reason: "my-llm provider routed"

Step 2 -- Verify it appears in the catalog

mvgc-gateway addon list
# ADDON                  LAYER       RULES  ENABLED
# core-identity          identity    2      true
# deny-unknown-host      connectors  1      true
# ...
# provider-my-llm        connectors  1      false   <- your new add-on

Step 3 -- Enable it

mvgc-gateway addon enable provider-my-llm
# Restart the gateway to activate

Rule file schema reference:

FieldTypeDescription
schemastringMust be mvgc.rule_file.v1
layerstringWhich evaluation layer these rules belong to (identity, connectors, etc.)
rules[]RuleList of rules -- same format as any other rule file (see section 3.3)

Guidelines for add-on authors:

  • Use a consistent naming convention: <category>-<name>.yaml (e.g., provider-openai.yaml, core-identity.yaml)
  • Choose a priority that fits within the existing range. Provider add-ons use priority 100; the catch-all deny (deny-unknown-host) uses priority 500
  • Add-ons should be self-contained -- avoid rules that depend on variables set by another add-on
  • Test with mvgc-gateway addon list to verify the rule count and layer are parsed correctly before enabling

4. Pushing policies at runtime

Axemere Console — Policy History

Update the active policy bundle

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

Inspect active policy bundle

curl http://localhost:7080/v1/admin/policies \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN"

5. Policy file locations by installation method

The gateway loads policy files from the directory pointed to by MVGC_POLICIES_DIR. The default path depends on how the gateway was installed. Credentials are managed separately via the admin API and are not written to disk by the installer.

Quick reference

Installation methodPolicy filesMain config (mvgc.yaml)
Linux .deb / .rpm/etc/mvgc/policies//etc/mvgc/mvgc.yaml
macOS Homebrew$(brew --prefix)/var/mvgc/configs/policies/$(brew --prefix)/etc/mvgc/mvgc.yaml
Docker Compose/configs/policies/ (baked into image; no bind mount)env vars in .env
Kubernetes / Helm/configs/policies/ (from ConfigMap)env vars + Kubernetes Secret
Tarball (manual)configs/policies/ (relative to working directory)mvgc.yaml (at tarball root)

Linux packages (.deb / .rpm)

The installer places all policy files under /etc/mvgc/policies/ and marks them config|noreplace, meaning package upgrades will not overwrite files you have edited.

/etc/mvgc/mvgc.yaml                          <- main config (config|noreplace)
/etc/mvgc/policies/bundle.yaml               <- skeleton bundle (loads from addons/ on startup)
/etc/mvgc/policies/available/                <- add-on catalog
/etc/mvgc/policies/available/core-identity.yaml
/etc/mvgc/policies/available/provider-openai.yaml
/etc/mvgc/policies/available/provider-anthropic.yaml
/etc/mvgc/policies/available/provider-gemini.yaml
/etc/mvgc/policies/available/provider-azure-openai.yaml
/etc/mvgc/policies/available/provider-cohere.yaml
/etc/mvgc/policies/available/deny-unknown-host.yaml
/etc/mvgc/policies/available/claude-cli-proxy.yaml
/etc/mvgc/policies/addons/                   <- active add-ons (symlinks into available/)
/etc/mvgc/policies/addons/core-identity.yaml
/etc/mvgc/policies/addons/provider-openai.yaml
/etc/mvgc/policies/addons/provider-anthropic.yaml
/etc/mvgc/policies/addons/provider-gemini.yaml
/etc/mvgc/policies/addons/provider-azure-openai.yaml
/etc/mvgc/policies/addons/provider-cohere.yaml
/etc/mvgc/policies/addons/deny-unknown-host.yaml
/etc/mvgc/credentials/credentials.yaml       <- credential seed (config|noreplace)
/etc/mvgc/workloads/workloads.yaml           <- workload seed (config|noreplace)
/usr/bin/mvgc-gateway                        <- binary
/lib/systemd/system/mvgc-gateway.service

The systemd service reads MVGC_POLICIES_DIR from /etc/default/mvgc-gateway. To point the gateway at a different policy directory, edit that file and restart the service:

# Edit the default config
sudo nano /etc/default/mvgc-gateway
# MVGC_POLICIES_DIR=/etc/mvgc/policies  <- change if needed

# Restart
sudo systemctl restart mvgc-gateway

# Or push a new bundle live without restarting
curl -X PUT http://localhost:7080/v1/admin/policies \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/yaml" \
  --data-binary @/etc/mvgc/policies/mvp_policy.yaml

macOS Homebrew

Homebrew installs the config under etc/ and policy files under var/ (following the convention that etc/ is for configuration and var/ is for mutable runtime data).

$(brew --prefix)/etc/mvgc/mvgc.yaml                                              <- main config
$(brew --prefix)/var/mvgc/configs/policies/bundle.yaml                          <- skeleton bundle
$(brew --prefix)/var/mvgc/configs/policies/available/                           <- add-on catalog
$(brew --prefix)/var/mvgc/configs/policies/available/core-identity.yaml
$(brew --prefix)/var/mvgc/configs/policies/available/provider-openai.yaml
$(brew --prefix)/var/mvgc/configs/policies/available/provider-anthropic.yaml
$(brew --prefix)/var/mvgc/configs/policies/available/provider-gemini.yaml
$(brew --prefix)/var/mvgc/configs/policies/available/provider-azure-openai.yaml
$(brew --prefix)/var/mvgc/configs/policies/available/provider-cohere.yaml
$(brew --prefix)/var/mvgc/configs/policies/available/deny-unknown-host.yaml
$(brew --prefix)/var/mvgc/configs/policies/available/claude-cli-proxy.yaml
$(brew --prefix)/var/mvgc/configs/policies/addons/                              <- active add-ons
$(brew --prefix)/var/mvgc/configs/policies/addons/core-identity.yaml           -> ../available/...
$(brew --prefix)/var/mvgc/configs/policies/addons/provider-openai.yaml         -> ../available/...
$(brew --prefix)/var/mvgc/configs/policies/addons/provider-anthropic.yaml      -> ../available/...
$(brew --prefix)/var/mvgc/configs/policies/addons/provider-gemini.yaml         -> ../available/...
$(brew --prefix)/var/mvgc/configs/policies/addons/provider-azure-openai.yaml   -> ../available/...
$(brew --prefix)/var/mvgc/configs/policies/addons/provider-cohere.yaml         -> ../available/...
$(brew --prefix)/var/mvgc/configs/policies/addons/deny-unknown-host.yaml       -> ../available/...
$(brew --prefix)/var/mvgc/configs/credentials/credentials.yaml                 <- credential seed
$(brew --prefix)/var/mvgc/configs/workloads/workloads.yaml                     <- workload seed
$(brew --prefix)/bin/mvgc-gateway                                               <- binary
$(brew --prefix)/var/log/mvgc/gateway.log                                       <- log output

On Apple Silicon the prefix is /opt/homebrew; on Intel it is /usr/local. To find your prefix: brew --prefix.

The launchd service sets MVGC_CONFIG to the installed mvgc.yaml. Edit that file to set your API key env vars and MVGC_ADMIN_TOKEN, then:

# Start (or restart) the service
brew services restart mvgc-gateway

# Or push a new bundle live without restarting
curl -X PUT http://localhost:7080/v1/admin/policies \
  -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \
  -H "Content-Type: application/yaml" \
  --data-binary @"$(brew --prefix)/var/mvgc/configs/examples/mvp_policy.yaml"

Docker Compose

The customer-facing compose files (docker-compose.yaml and docker-compose.postgres.yaml) do not bind-mount a host configs/ directory. Policy files are baked into the image at /configs/policies/ and used as-is. Configuration is supplied entirely through environment variables in .env.

To push a policy bundle at runtime (no restart needed):

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

To supply custom policy files via a volume mount, use docker run directly (see the Docker and Podman guide for the three mount options).

To restart the gateway to pick up .env changes:

docker compose restart gateway

Kubernetes / Helm

The Helm chart stores policy files in a Kubernetes ConfigMap and mounts it at /configs/policies/ inside the gateway pod. MVGC_POLICIES_DIR is set to /configs/policies by default.

Kubernetes resource                 Mount path in pod
─────────────────────────────────── -> ────────────────────────────
ConfigMap: <release>-policies       /configs/policies/
  bundle.yaml, available/*.yaml,
  addons/*.yaml (symlinks)
ConfigMap: <release>-credentials    /configs/credentials/
  credentials.yaml
ConfigMap: <release>-workloads      /configs/workloads/
  workloads.yaml
Secret: (user-managed)              env vars injected via envFrom
  DATABASE_URL, MVGC_ADMIN_TOKEN

To update policy files:

# Push a bundle live via admin API (no restart required)
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

Credentials are never stored in the ConfigMap. Inject them as environment variables via a Kubernetes Secret or an external secrets manager, referenced in values.yaml under adminToken.existingSecret and extraEnv.


Tarball (manual install)

The Linux release tarball extracts to a directory containing the binary, the main config, and the policy files:

mvgc-gateway                                             <- binary
mvgc.yaml                                                <- main config
configs/policies/bundle.yaml                             <- skeleton bundle
configs/policies/available/core-identity.yaml            <- add-on catalog
configs/policies/available/provider-openai.yaml
configs/policies/available/provider-anthropic.yaml
configs/policies/available/provider-gemini.yaml
configs/policies/available/provider-azure-openai.yaml
configs/policies/available/provider-cohere.yaml
configs/policies/available/deny-unknown-host.yaml
configs/policies/available/claude-cli-proxy.yaml
configs/policies/addons/core-identity.yaml               <- active add-ons (symlinks)
configs/policies/addons/provider-openai.yaml
configs/policies/addons/provider-anthropic.yaml
configs/policies/addons/provider-gemini.yaml
configs/policies/addons/provider-azure-openai.yaml
configs/policies/addons/provider-cohere.yaml
configs/policies/addons/deny-unknown-host.yaml
configs/credentials/credentials.yaml                     <- credential seed
configs/workloads/workloads.yaml                         <- workload seed

Set MVGC_POLICIES_DIR to the absolute path of the configs/policies/ directory before starting the gateway:

export MVGC_POLICIES_DIR=/opt/mvgc/configs/policies
export MVGC_ADMIN_TOKEN=your-admin-token
export DATABASE_URL=postgres://...
./mvgc-gateway

See also

  • Configuration Overview -- how credentials, workloads, and policies interrelate
  • Credentials -- credential reference: provider values, modes, secret resolution
  • Workloads -- workload reference: default attribution, ingress modes