Skip to content

shield360 proxy

shield360 proxy serve is a local AI gateway. Apps, SDKs and LiteLLM send their provider calls to it instead of straight to the provider. Before a request leaves, it enforces your organisation’s Shield360 AI policy, which is the same policy the Shield360 browser extension enforces:

  • AI tools. A provider your organisation blocks (aiTools) is refused. One set to warn is forwarded and recorded.
  • Approved models. When the organisation warns or blocks outside the approved list, a request for a model or provider not on it (approvedModels) is refused with model_not_approved, or forwarded and recorded.
  • Prompt protection. Sensitive data in a prompt (aiGuard, with per-kind overrides in aiGuardKinds) is warned about, masked or blocked.
  • Prompt injection. The newest turn of each request, meaning the new user message and any tool results the client sends back, is checked with the same rules as the coding-agent hooks. It follows the organisation’s prompt-injection setting for coding agents (injectionMode): warn forwards the request and records a detection, block refuses it with prompt_injection. Earlier turns and system prompts are not checked. The hosted proxy does not check for prompt injection yet. See Prompt-injection protection.
  • Evidence. Each decision is reported to Shield360 as a guard event with source AI proxy. The event records the kinds of data and their keyed fingerprints, shown as tokens such as [api_key:fp_3f9c2a1b]. It never contains any character of the values. See Data retention.

This is API gateway mode: clients opt in by changing their base URL, and there is no TLS interception.

There are two ways to run it:

  • Hosted by Shield360 (Shield360 SaaS). Shield360 runs the proxy at https://proxy.shield360.ai. There’s nothing to install: create a proxy key and change your apps’ base URL. See Hosted proxy.
  • Run it yourself: on a machine, as a service, or as a container in your own network. The rest of this page covers that.

On Shield360 SaaS, the AI proxy panel under Settings → Collection & deployment has a Hosted by Shield360 tab, shown first. Self-hosted deployments don’t show it.

Create a proxy key

Give the key a name and, optionally, an expiry. The key starts with s360p_ and is shown once. Shield360 stores only its SHA-256 hash. A key belongs to the environment that is selected in the header, and its requests are enforced with that environment’s policy and recorded in its data.

Point your apps at it

The key is part of the base URL. Apps keep sending their own provider API key, as usual:

Terminal window
OPENAI_BASE_URL=https://proxy.shield360.ai/t/<proxy-key>/openai/v1
ANTHROPIC_BASE_URL=https://proxy.shield360.ai/t/<proxy-key>/anthropic
GOOGLE_GEMINI_BASE_URL=https://proxy.shield360.ai/t/<proxy-key>/gemini

For LiteLLM, set api_base: https://proxy.shield360.ai/t/<proxy-key>/openai/v1. The panel fills your key into these while it’s shown.

Check the decisions

Decisions appear under Detect & respond → Detections → Guard events with source AI proxy and host hosted-proxy, and in Govern → Policies & rules → Violations. The panel shows when each key was last used.

To stop a key, revoke it in the panel. The proxy stops accepting it within a minute. A key past its expiry stops the same way.

  • Data flow. Requests pass through Shield360’s proxy in AWS us-east-2 (Ohio). TLS ends at Shield360’s load balancer. Prompts and your provider API keys are inspected in memory and passed on to the provider. They’re never stored or logged.
  • What’s logged. The method, the provider, the path after the provider name, the status, the action and the kinds found. Never the proxy key or anything under /t/, request or response bodies, query strings, or Authorization, x-api-key, api-key, x-goog-api-key or cookie headers.
  • What’s recorded. The same guard event as a self-run proxy: kinds and keyed fingerprints. Never any character of the values.
  • What the provider sees. Your API key and your request, with sensitive data masked or the request refused, as your policy says. X-Shield360-* headers and the proxy key are removed first. The request comes from Shield360’s IP addresses. If your provider account only accepts requests from your own IP addresses, run the proxy yourself.
  • Fixed providers. The hosted proxy forwards only to api.openai.com, api.anthropic.com and generativelanguage.googleapis.com. For Azure OpenAI, LiteLLM or another upstream, run the proxy yourself, or put the hosted proxy in front of LiteLLM’s provider calls.

Each organisation environment has its own limits, so one customer can’t slow down another:

LimitDefaultWhen it’s reached
Requests in flight32429, provider error shape, Retry-After: 1
Request rate20 per second, bursts of 40429, provider error shape, Retry-After: 1
Concurrent inspectionsA quarter of the proxy’s scan slotsThe prompt counts as not inspected (see Scan limits)

A key the proxy can’t verify gets 401 with code proxy_key_invalid, in the provider’s error shape. The response doesn’t say whether the key is unknown, revoked or expired.

If the proxy can’t reach Shield360, it keeps enforcing each key’s last known policy for up to 10 minutes. It refuses the key’s requests with policy_unavailable if that policy blocks by default and has Block when the policy is unavailable on. A key it has never verified gets 503.

The hosted proxy is shield360 proxy serve --hosted, built from deploy/proxy/Dockerfile and deployed by deploy/k8s/shield360-{qa,prod}/hosted-proxy.yaml:

Terminal window
shield360 proxy serve --hosted \
--platform-url http://shield360-service.shield360-prod.svc.cluster.local \
--service-secret-file /etc/shield360-hosted-proxy/secret \
--listen 0.0.0.0:8787 --shutdown-delay 15s
  • Keys. The proxy sends each key to POST /api/internal/hosted-proxy/resolve, in batches of up to 100, with the service secret in X-Shield360-Hosted-Proxy-Secret. It gets back an opaque tenant id (a keyed hash, not the database config id), the AI policy and a TTL of at most 60 seconds. Unknown keys are cached for 30 seconds. Concurrent lookups of one key share one request.
  • Events. Decisions are queued in memory, up to 20,000 across tenants. When the queue is full the oldest are dropped, and the count shows in the stats: line the proxy logs every minute. They’re delivered every 2 seconds to POST /api/internal/hosted-proxy/events, one batch per key. The app finds each batch’s tenant from its key again, and forces source proxy and host hosted-proxy.
  • Service secret. SHIELD360_HOSTED_PROXY_SECRET, at least 32 characters, must be the same for the app and the proxy. Without it the internal routes answer 404 and the panel hides the hosted tab. The app also needs SHIELD360_HOSTED_PROXY_URL, and optionally SHIELD360_HOSTED_PROXY_REGION for the data-flow note.
  • TLS. The proxy serves plain HTTP. Put it behind a TLS-terminating load balancer, such as the ALB in the manifests. It needs no enrolment and no access token, because the proxy key is the credential.
  • Health. GET /healthz is liveness. GET /readyz returns 503 while the proxy is draining after SIGTERM.
FlagDescription
--hostedMulti-tenant mode. --upstream, --upstream-vendor, --config, --access-token-file, --install-key-file and --url are refused.
--platform-urlThe Shield360 app. Must use https, or plain http only for a loopback or in-cluster (.svc) address. SHIELD360_URL also works.
--service-secret-fileFile that holds the service secret. SHIELD360_HOSTED_PROXY_SECRET also works.
--tenant-concurrency, --tenant-rps, --tenant-burstPer-tenant limits. Defaults 32, 20 and 40.
--tenant-scan-shareConcurrent inspections per tenant. Default a quarter of --scan-concurrency, at least 1.
--global-concurrencyRequests in flight across every tenant. Default 2048. Above it, 503.
--event-queueDecisions held in memory for delivery. Default 20,000.
--shutdown-delayOn SIGTERM, how long /readyz reports draining before the listener closes.

To run the same binary as one customer’s dedicated proxy, add --pin-tenant <tenant id>: keys of any other tenant are refused.

What the proxy enforces is set in Shield360, in the organisation’s enforcement policy under Govern → Policies & rules: the AI proxy column of the Sensitive data tab (and the All channels column, which it follows unless it sets its own value), the AI tools tab, the Block when the policy is unavailable toggle, and the Coding agents prompt-injection setting on the Coding agents tab. The organisation sets them once; an environment can override any one of them. The proxy is handed the policy of the environment its key or enrolment belongs to.

The platform sends the proxy’s own actions as proxyGuard and proxyGuardKinds beside the browser extension’s aiGuard and aiGuardKinds. A proxy older than that reads aiGuard and aiGuardKinds, which are the browser’s values. The hosted proxy always gets the proxy’s values.

Deployment stays on Settings → Collection & deployment, which has an AI proxy panel with:

  • Connected proxies: each proxy in the current environment, with its version, whether it serves only its own machine or other machines, the policy version it enforces and when it fetched it, when it was last seen, and what it inspected and blocked since it started. A proxy that has not reported for five minutes shows as stale. You can revoke a proxy here.
  • Settings that apply: a summary of the parts of the policy the proxy enforces, each linking to its tab in Govern → Policies & rules.
  • Setup: the commands on this page, with your Shield360 address and install key filled in, for a machine or a container, and the base URLs for your apps.

Some settings are local to each proxy and are set with flags or a config file on the proxy host: its upstreams, listen address, access token and scan limits.

Enrol

On a machine whose collector is enrolled, the proxy uses that enrolment and there’s nothing to do. Otherwise, enrol the proxy as its own device with your organisation’s install key:

Terminal window
SHIELD360_INSTALL_KEY=<key> shield360 proxy enroll --url https://app.shield360.ai

The proxy enrols with kind proxy and appears in the fleet list as AI proxy. Its token is kept in <config dir>/proxy/proxy-enrollment.json with owner-only permissions. The install key isn’t stored.

Serve
Terminal window
shield360 proxy serve

The proxy listens on 127.0.0.1:8787. It fetches the policy before it starts serving, then fetches it again every 60 seconds.

To keep it running in the background instead, see Run it as a service.

Point your clients at it

Requests to /<upstream>/<path> go to <upstream URL>/<path>. The query string and credentials pass through unchanged.

UpstreamDefault URLBase URL to use
openaihttps://api.openai.comhttp://127.0.0.1:8787/openai/v1
anthropichttps://api.anthropic.comhttp://127.0.0.1:8787/anthropic
geminihttps://generativelanguage.googleapis.comhttp://127.0.0.1:8787/gemini
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8787/openai/v1") # OPENAI_API_KEY as usual
from anthropic import Anthropic
client = Anthropic(base_url="http://127.0.0.1:8787/anthropic") # ANTHROPIC_API_KEY as usual
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "http://127.0.0.1:8787/openai/v1" });
from google import genai
client = genai.Client(http_options={"base_url": "http://127.0.0.1:8787/gemini"})

Most SDKs also read the base URL from the environment, for example OPENAI_BASE_URL=http://127.0.0.1:8787/openai/v1 or ANTHROPIC_BASE_URL=http://127.0.0.1:8787/anthropic.

To send LiteLLM’s provider calls through the proxy, set api_base on each model:

model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_base: http://127.0.0.1:8787/openai/v1
api_key: os.environ/OPENAI_API_KEY
- model_name: claude-sonnet
litellm_params:
model: anthropic/claude-sonnet-4-5
api_base: http://127.0.0.1:8787/anthropic
api_key: os.environ/ANTHROPIC_API_KEY

You can also put the proxy in front of LiteLLM, so apps call the proxy and the proxy calls LiteLLM. LiteLLM speaks OpenAI’s format. Give it a vendor id so aiTools can decide on it; otherwise the default entry applies:

8787/litellm/v1
shield360 proxy serve --upstream litellm=http://litellm:4000 --upstream-vendor litellm=openai

An app that serves many people can name the person each request is for with the X-Shield360-User header. The proxy records that name on the event and removes the header before forwarding. Without the header, events name the enrolled user.

To point every machine at the proxy without setting each one up by hand, push the configuration through Intune, Jamf or your configuration tool. Settings → Data collection → Deployment → AI proxy → Push with MDM generates each file below with the base URL filled in. Choose which proxy to point at:

ProxyBase URLToken
Hosted by Shield360https://<hosted-proxy>/t/<proxy-key> (the key you just created is filled in)In the URL
Localhttp://127.0.0.1:8787 (each machine runs its own proxy)None
Container or another machineThe URL you enter, for example http://proxy.internal:8787X-Shield360-Proxy-Token header

The examples below use <base> for the base URL.

Claude Code reads a managed settings file that overrides every user and project setting. Set ANTHROPIC_BASE_URL in its env block:

{
"env": {
"ANTHROPIC_BASE_URL": "<base>/anthropic"
}
}

Install it at:

OSPath
macOS/Library/Application Support/ClaudeCode/managed-settings.json
Linux and WSL/etc/claude-code/managed-settings.json
WindowsC:\Program Files\ClaudeCode\managed-settings.json

Claude Code no longer reads the old Windows path C:\ProgramData\ClaudeCode\managed-settings.json. On macOS you can push the same setting as a configuration profile for the com.anthropic.claudecode preferences domain instead; the panel generates a .mobileconfig. See Claude Code’s managed settings documentation.

ANTHROPIC_BASE_URL can’t carry a header. For a container proxy with an access token, add ANTHROPIC_CUSTOM_HEADERS (Claude Code 2.1.227 or later):

{
"env": {
"ANTHROPIC_BASE_URL": "http://proxy.internal:8787/anthropic",
"ANTHROPIC_CUSTOM_HEADERS": "X-Shield360-Proxy-Token: <access-token>"
}
}

Codex reads config.toml for each user ($CODEX_HOME, ~/.codex by default), so push it to each user profile. Point the built-in OpenAI provider at the proxy:

openai_base_url = "<base>/openai/v1"

The built-in provider can’t send extra headers. For a container proxy with an access token, define a provider instead:

model_provider = "shield360"
[model_providers.shield360]
name = "Shield360 AI proxy"
base_url = "http://proxy.internal:8787/openai/v1"
env_key = "OPENAI_API_KEY"
wire_api = "responses"
http_headers = { "X-Shield360-Proxy-Token" = "<access-token>" }

You can also set OPENAI_BASE_URL machine-wide, as below. Recent Codex releases prefer openai_base_url, so check that the variable takes effect with the Codex version you run.

The OpenAI and Anthropic SDKs, and LiteLLM, read their base URL from the environment. Set these for every user:

OPENAI_BASE_URL=<base>/openai/v1
ANTHROPIC_BASE_URL=<base>/anthropic
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment]
"OPENAI_BASE_URL"="<base>/openai/v1"
"ANTHROPIC_BASE_URL"="<base>/anthropic"
Terminal window
[Environment]::SetEnvironmentVariable('OPENAI_BASE_URL', '<base>/openai/v1', 'Machine'); [Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL', '<base>/anthropic', 'Machine')
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.shield360.proxy-env</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>launchctl setenv OPENAI_BASE_URL '&lt;base&gt;/openai/v1'; launchctl setenv ANTHROPIC_BASE_URL '&lt;base&gt;/anthropic'</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Terminal window
OPENAI_BASE_URL="<base>/openai/v1"
ANTHROPIC_BASE_URL="<base>/anthropic"

Tools started after the change pick the variables up; users may need to sign out and back in. SDKs read only the base URL from the environment, not the access token header. For a container proxy with a token, add the header in the app, or use the hosted proxy, which carries its key in the URL.

The configuration covers Claude Code, Codex, and apps and scripts that use the OpenAI or Anthropic SDKs or LiteLLM. It doesn’t cover Cursor, GitHub Copilot, the ChatGPT and Claude desktop apps, or web chat. Use the browser extension for those; a forward proxy is coming. For GitHub Copilot’s agent (Copilot CLI, agent mode in VS Code and the cloud agent), the coding-agent hooks and the GitHub Copilot connector cover what can be seen; see GitHub Copilot.

Terminal window
shield360 proxy install [serve flags]
shield360 proxy status
shield360 proxy uninstall

install registers shield360 proxy serve with your user’s service manager, with the flags you give it. It needs no administrator rights.

PlatformServiceStartsRestartsLogs
macOSLaunchAgent ai.thinkfleet.shield360-proxyAt loginWhenever it exits~/Library/Logs/Shield360/proxy.log
Linuxsystemd user service shield360-proxyAt boot (install enables lingering; if that is refused, run sudo loginctl enable-linger <user>)On failurejournalctl --user -u shield360-proxy
WindowsTask Scheduler task Shield360ProxyAt logonOn failure, every minute%LOCALAPPDATA%\Shield360\Logs\proxy.log

Log files are rotated at 10 MB. They never contain prompts, answers, credentials or detected values.

Enrol first, or install on a machine whose collector is enrolled. On Linux without a systemd user manager, run serve under your own supervisor or use the container. Paths are made absolute, and secrets are never written into the service definition: pass the access token with --access-token-file.

status shows:

  • whether the service is installed and running
  • whether the proxy answers, its address, and whether it requires an access token
  • which enrolment it uses
  • the policy version in force and when it was last fetched
  • when it last sent its heartbeat
  • its upstreams
  • what it inspected, blocked and could not inspect since it started

The running proxy writes this to <state dir>/proxy-status.json every minute.

Every 60 seconds the proxy reports its status to Shield360, which the Connected proxies list shows. The heartbeat carries the proxy’s version, the policy version and when it was fetched, whether it listens locally or for other machines, its upstream names, and how many requests it inspected and blocked since it started. It never carries upstream URLs, credentials or anything from a request.

The image runs shield360 proxy serve --listen 0.0.0.0:8787 --allow-remote as a non-root user. No registry publishes it yet, so build it from the Shield360 repository:

Terminal window
docker build -f deploy/proxy/Dockerfile -t shield360-proxy:local .

It is configured by its environment:

VariablePurpose
SHIELD360_URLYour Shield360 address
SHIELD360_INSTALL_KEYInstall key. The proxy enrols on first start, and again if its token is revoked.
SHIELD360_PROXY_ACCESS_TOKENThe token clients must send. Required.

Keep /var/lib/shield360-proxy on a volume. It holds the enrolment, the policy cache and undelivered events, so the proxy keeps one device identity across restarts.

Terminal window
docker run -d --name shield360-proxy --restart unless-stopped \
-p 8787:8787 \
-e SHIELD360_URL=https://app.shield360.ai \
-e SHIELD360_INSTALL_KEY=<key> \
-e SHIELD360_PROXY_ACCESS_TOKEN=<token> \
-v shield360-proxy-state:/var/lib/shield360-proxy \
shield360-proxy:local

Serve flags go after the image name, for example --upstream litellm=http://litellm:4000 --upstream-vendor litellm=openai. deploy/proxy/docker-compose.yml is a Compose sample. deploy/proxy/kubernetes.yaml is a minimal Deployment and Service that reads a Secret and is ready when /healthz answers.

A proxy that other machines can reach sends provider calls for anyone who can connect to it. With --allow-remote, it requires an access token, and it refuses to start without one. The token is read from the file named by --access-token-file, or from SHIELD360_PROXY_ACCESS_TOKEN. It is never read from a flag value and never logged. It must be at least 16 printable characters. To make one:

Terminal window
openssl rand -base64 32 | tr -d '=+/' > proxy-token && chmod 600 proxy-token

The Generate token button on the AI proxy panel makes one in your browser. It is shown once and never sent to Shield360.

Clients send the token in either header:

X-Shield360-Proxy-Token: <token>
Proxy-Authorization: Bearer <token>

It is not the Authorization header, which carries the provider’s API key and passes through unchanged. The proxy compares tokens in constant time and removes both headers before forwarding. A request without a valid token gets 401 in the provider’s error format, or 407 when it used Proxy-Authorization. /healthz needs no token, so it works as a readiness probe.

import os
from openai import OpenAI
client = OpenAI(
base_url="http://shield360-proxy:8787/openai/v1",
default_headers={"X-Shield360-Proxy-Token": os.environ["SHIELD360_PROXY_ACCESS_TOKEN"]},
)

For LiteLLM, add the header to each model:

litellm_params:
api_base: http://shield360-proxy:8787/openai/v1
extra_headers:
X-Shield360-Proxy-Token: os.environ/SHIELD360_PROXY_ACCESS_TOKEN

Checking a prompt runs the detector’s regular expressions over its text. On adversarial text this costs roughly a second of CPU per megabyte, so each request has an inspection budget:

LimitDefaultFlag
Prompt text inspected per request2 MB--max-scan-mb
Time one request’s inspection may take, including waiting for a slot500 ms--scan-timeout
Inspections running at onceNumber of CPUs, at least 2--scan-concurrency

A scan that runs past its time keeps its slot until it finishes, so scanning never uses more than the concurrency limit’s worth of CPU.

A request over the budget is not inspected:

  • When Prompt protection is set to Block, the request is refused with 413 in the provider’s error format and code too_large_to_inspect. A blocked event is recorded.
  • Otherwise the request is forwarded unchanged, and a warned event is recorded with the kind uninspected. It shows as Not inspected (too large) in Detect & respond → Detections → Guard events.
ActionRequestResponseEvent
off, or nothing foundForwarded unchangedUnchangedNone
warnForwarded unchanged. There is no person to ask, so the proxy records the request and lets it through.Unchangedwarned
maskEach value is replaced with a placeholder such as [EMAIL_1] or [PAYMENT_CARD_1] before it is forwardedPlaceholders are replaced with the original valuesredacted
blockRefused with 403 in the provider’s own error format. The message names the kinds found, never the values.—blocked

The action for a request is the strictest of the actions for the kinds it contains. Each kind uses its aiGuardKinds entry, or aiGuard when it has none. The proxy detects the same kinds as the browser extension: API keys, private keys, passwords, connection strings, social security numbers, payment cards, tokens, cloud credentials, email addresses, phone numbers and IBANs. IP addresses are checked only when you pass --detect-ip or the policy names ip_address, because version numbers look like addresses.

A refusal uses the provider’s error shape, so SDKs raise their usual error:

{"error": {"message": "Shield360 blocked this request: ...", "type": "shield360_blocked", "code": "sensitive_data"}}

Anthropic clients get {"type":"error","error":{"type":"permission_error",...}} and Gemini clients get {"error":{"code":403,"status":"PERMISSION_DENIED",...}}.

With mask, the provider sees only placeholders. Numbering is per kind and the same value always gets the same placeholder, so the model can still refer to “[EMAIL_1]” consistently. The placeholders match those the browser extension writes.

The proxy keeps the map from placeholders to values in memory for that one request, and discards it once the response has been sent. Before it goes to your client, the response is rewritten:

  • Non-streaming JSON. Every string in the answer is restored, including tool-call arguments.
  • Streaming (server-sent events). This covers OpenAI Chat Completions and Responses, Anthropic Messages and Gemini streamGenerateContent, including Gemini’s JSON-array form without alt=sse. A model can emit one placeholder across several deltas ("[EMA", "IL_1]"), so each stream holds back a trailing fragment that could still become a placeholder, up to the length of the longest one. It releases the fragment as soon as the next delta completes the placeholder or shows it was ordinary text. Anything still held back at the end is sent before the stream finishes.

Values in tool-call arguments are masked inside their JSON strings, so the arguments stay valid JSON.

  • The proxy caches the last policy it fetched on disk, with owner-only permissions, and uses it when Shield360 can’t be reached.
  • A 401 from Shield360 means the token was revoked. When an install key is available, from SHIELD360_INSTALL_KEY or --install-key-file, the proxy enrols again and continues.
  • The proxy fails closed only when it has no policy at all and the last policy it saved had guardFailClosed on with aiGuard set to block. It then refuses every request with a policy_unavailable error. In every other case it forwards requests unchecked and logs a warning. The warning never includes content.
  • Events it can’t deliver are queued in <state dir>/guard-events-pending.jsonl, with owner-only permissions, and sent with the next batch.

GET /healthz reports whether a policy is in force and its version.

The single-tenant proxy also applies budget enforcement. After the approved-models check it asks Shield360’s budget decision for the person (X-Shield360-User, else the enrolled user), the vendor and the model, with the prompt’s input priced at the model’s list price as the request’s estimate. Decisions are cached for 60 seconds per person, vendor and model under <state dir>/budget.

DecisionProxy
AllowForwards.
WarnForwards, with X-Shield360-Budget-Warning on the response.
ThrottleForwards within the person’s rate; past it, 429 with code budget_throttled.
Block403 with code budget_exceeded, naming who to ask and the override link.

If Shield360 can’t be reached it forwards (fails open), unless the last decision said to fail closed or SHIELD360_BUDGET_FAIL_CLOSED=1 is set; then it refuses with budget_unavailable. SHIELD360_BUDGET_ENFORCEMENT=off turns the check off on this machine. The hosted proxy does not apply budgets yet.

When an AI agent calls a model through the proxy, it can send its Shield360 agent token (Govern → Agent access) so the call is decided by its agent authorization profile: the approved models, the environment’s approved-model list, the run context, the person it acts for, and its daily and monthly spend limits. The local proxy, the container and the hosted proxy all do this.

HeaderMeaning
X-Shield360-Agent-TokenThe agent’s token (s360a_…). It marks the request as the agent’s.
X-Shield360-On-Behalf-OfOptional. The person the agent acts for. Without it, X-Shield360-User is used.
X-Shield360-EnvironmentOptional. The run context, such as production.
Terminal window
curl https://proxy.shield360.ai/t/$SHIELD360_PROXY_KEY/openai/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "X-Shield360-Agent-Token: $SHIELD360_AGENT_TOKEN" \
-H "X-Shield360-Environment: production" \
-H "Content-Type: application/json" \
-d '{ "model": "gpt-4o-mini", "max_tokens": 800, "messages": [{ "role": "user", "content": "Draft the release notes." }] }'

After the budget check, the proxy asks Shield360’s check API with the agent’s token, the model, the provider, the prompt’s estimated input tokens and the request’s output limit (max_tokens, max_completion_tokens, max_output_tokens or Gemini’s maxOutputTokens). The proxy also proves which environment it enforces for: its collector token, or for the hosted proxy the service secret and the proxy key in the base URL. Shield360 refuses an agent token from any other environment.

DecisionProxy
AllowForwards. The call is counted against the agent’s spend limits.
Deny403 with code agent_not_authorized, the reason and the decision id.
Needs approval403 with code agent_needs_approval. The agent can ask for the scope through POST /api/agent-authorization/request-access.
Too many checks429 with code rate_limited and Retry-After.

The proxy checks every allowed call again, because each one is counted against the spend limits and a kill switch must stop the next call. It reuses a refusal for 30 seconds for the same agent, model, provider, environment and person. A request with a token that is not an agent token is refused. So is an agent’s request when the proxy cannot tell which model it is for, unless it is a GET such as listing models.

If Shield360 can’t be reached, the proxy follows the policy’s fail mode. When guardFailClosed is on and aiGuard is block, it refuses with agent_authorization_unavailable. Otherwise it forwards with X-Shield360-Agent-Warning on the response and records a warning. SHIELD360_AGENT_AUTHORIZATION=warn forwards every refusal with that warning instead (useful while you roll it out), and SHIELD360_AGENT_AUTHORIZATION=off turns the check off on this machine.

Refusals and warnings are reported as guard decisions named agent-authorization. Every answer is also in the agent’s decision log. The agent token and the two context headers are removed before the request is forwarded, and they are never logged.

The proxy never logs or stores Authorization, x-api-key, api-key, x-goog-api-key or cookie headers, request or response bodies, query strings (Gemini keys travel there), or detected values. It passes credential headers through to the provider unchanged and strips hop-by-hop headers.

Terminal window
shield360 proxy serve [flags]
FlagDescription
--listenAddress to listen on. Default 127.0.0.1:8787. An address other machines can reach is refused unless you also pass --allow-remote.
--allow-remoteAllow a non-loopback --listen address. Requires an access token.
--access-token-fileFile that holds the access token clients must send. SHIELD360_PROXY_ACCESS_TOKEN also works.
--upstream name=URLAdd an upstream or replace a default one. Repeatable.
--upstream-vendor name=vendorThe AI tool id aiTools uses for an upstream. The defaults are openai, anthropic and google. For another upstream, the vendor id defaults to the upstream’s name.
--configYAML or JSON file with the same settings. Flags override it.
--state-dirWhere the enrolment, policy cache and undelivered events are kept. Default <config dir>/proxy.
--detect-ipAlso detect IP addresses.
--max-body-mbLargest request body accepted. Default 20. Larger bodies get 413.
--max-scan-mb, --scan-timeout, --scan-concurrencyThe scan limits.
--log-fileWrite the log to this file, rotated at 10 MB, instead of stderr.
--url, --install-key-fileEnrol on start when the proxy isn’t enrolled yet, and enrol again after a 401. SHIELD360_URL and SHIELD360_INSTALL_KEY also work.

A config file:

listen: 127.0.0.1:8787
detectIP: false
maxBodyMB: 20
maxScanMB: 2
scanTimeout: 500ms
accessTokenFile: /etc/shield360/proxy-token # the token itself never goes here
upstreams:
litellm:
url: http://litellm:4000
vendor: openai
azure:
url: https://my-resource.openai.azure.com
Terminal window
SHIELD360_INSTALL_KEY=<key> shield360 proxy enroll --url <shield360-url> [--install-key-file <path>] [--state-dir <dir>]
Terminal window
shield360 proxy status [--state-dir <dir>]

Shows the service, whether the proxy is running, its enrolment, the policy in force and when it was fetched, its upstreams and counters. See Run it as a service.

The state directory defaults to <config dir>/proxy. SHIELD360_PROXY_STATE_DIR changes it.

  • API traffic only. The proxy sees requests that are sent to it. Browser traffic to ChatGPT, Claude or Gemini is covered by the browser extension.
  • No TLS interception yet. A forward proxy that desktop apps and CLIs use without changing their configuration, routed with a PAC file and a per-deployment CA, is the next phase.
  • One identity per proxy. Events name the enrolled user unless the app sets X-Shield360-User. Per-user proxy authentication is planned.
  • JSON bodies only. Multipart uploads (audio, files) and bodies in an encoding other than gzip are not inspected. With prompt protection or prompt-injection checks on, a request with an unsupported Content-Encoding is refused with 415.
  • Large prompts are not inspected. Prompts over the scan limits are refused when prompt protection blocks, and otherwise forwarded and recorded as not inspected.
  • No published image yet. Build the container from the repository. Shield360 SaaS customers can use the hosted proxy instead.