Security #
ZigAI keeps policy close to each boundary. Defaults protect ordinary hosted provider use. Local services and unusual networks require an explicit opt-in.
Outbound URLs #
UrlPolicy applies to provider endpoints, MCP Streamable HTTP endpoints, and
rich-content URLs a provider may fetch. By default it:
- accepts HTTPS only;
- rejects embedded usernames and passwords;
- rejects local names, single-label hosts, and non-public literal IPs;
- permits any remaining host.
Use an exact host allowlist when the destination set is known:
const policy = zigai.UrlPolicy{
.allowed_hosts = &.{ "api.openai.com", "files.example.com" },
};
var http = zigai.transport.HttpTransport.initWithOptions(allocator, io, .{
.url_policy = policy,
});
defer http.deinit();
const agent = zigai.Agent{
.model = client.model(),
.url_policy = policy,
};
For a deliberate local development endpoint, set both allow_http and
allow_local_network. Provider CLIs do this only when an operator supplies a
custom base URL.
The standard HTTP transport never follows redirects. Its default rejects 3xx;
.return_response lets an application inspect the response without contacting
the Location target. This prevents authentication headers from crossing to a
different origin implicitly.
URL validation happens before DNS. It blocks accidental local literals and names, but it cannot prevent a hostile public hostname from resolving to a private address. For hostile inputs, combine an exact host allowlist with network egress controls or a transport whose resolver validates every resolved address.
MCP HTTP authorization #
MCP authorization is optional. When enabled, ZigAI follows the
2026-07-28 authorization profile
at the transport boundary:
protectedResourceDiscoveryUrlsreturns the endpoint-path and root RFC 9728 URLs in required order;parseProtectedResourceMetadataaccepts extensions but requires the canonical resource, at least one distinct issuer, and the Bearer header method;authorizationServerDiscoveryUrlsreturns RFC 8414 and OIDC URLs in required path-aware order; parsed metadata must match the selected issuer exactly, use eligible HTTPS endpoints, and advertiseS256PKCE;ClientPolicybinds one canonical resource and authorization-server issuer to aTokenProvider. The callback receives the RFC 8707resource, method, accumulated scopes, and refresh reason. Return tokens withAccessToken.initAllocso partial allocation failures are cleaned safely;- Streamable HTTP adds
Authorization: Bearer ...to every POST. A valid 401 challenge can request a bounded refresh; a 403insufficient_scopechallenge can request a bounded stable-order scope union. Static Authorization headers cannot be combined with a token policy; ServerPolicypasses only the token value, canonical audience, method, and params to the application verifier. The verifier must validate signature, expiry, issuer, and audience before returningauthorized. Denials produce an ownedWWW-Authenticateresponse header; callback descriptions and token bytes are never reflected.
Browser and network checks are separate in DeploymentPolicy. It rejects
cleartext unless explicitly enabled, requires an exact allowed Origin whenever
the header is present, and can require an exact Host value. Duplicate Origin,
Host, and Authorization headers fail closed. This implements the
Streamable HTTP Origin requirement
before JSON parsing. Bind local servers to loopback; use a reverse proxy or
host framework that supplies truthful HttpMetadata.is_tls, preserves response
headers, and validates resolved addresses at the network boundary.
ServerResponse.headers is borrowed until deinit; an HTTP host must copy or
write those headers first. The standard outbound HTTP transport does not follow
redirects, so Bearer credentials cannot cross origins implicitly. Custom HTTP
transports must preserve that rule and mark Authorization headers sensitive.
Credentials and diagnostics #
Provider clients place API keys only in authentication headers. Standard names
such as Authorization, X-Api-Key, cookies, and names containing token,
secret, or ending in -key are sensitive even if a caller forgets to mark
them. Custom transports receive raw values because they must send the request;
they are trusted application code. Diagnostic code should use
Header.redactedValue().
Lifecycle and telemetry events never contain provider authentication headers or API keys. Prompt capture remains disabled by default because prompts and tool data may themselves contain secrets. Treat a telemetry exporter as a trusted sink when capture is enabled.
Provider error bodies are hidden by default and bounded when enabled. Native
and compatible adapters always suppress their configured API key if a provider
echoes it in a body, parsed message, or code. ProviderError.sensitive_data_redacted
reports that suppression.
Trust boundaries #
| Boundary | Treat as | Protection and responsibility |
|---|---|---|
| Prompts and history | Untrusted data | JSON and context limits apply. They never execute directly. Prompt injection remains an application concern. |
| Tool arguments and results | Untrusted data | JSON, size, timeout, queue, and concurrency limits apply. Tool implementations are trusted code and must authorize side effects. |
| Provider and MCP endpoints | Operator configuration | URL policy runs before callbacks and socket work. Prefer exact host allowlists. |
| MCP servers and discovered tools | Untrusted peers | Protocol, capability, schema, pagination, round-trip, and JSON limits apply. Review tool descriptions and requested actions before granting authority. |
| MCP OAuth metadata and challenges | Untrusted network data | Bounded parsers, URL policy, exact issuer/resource binding, PKCE checks, and retry limits apply. Token storage and interactive authorization remain application-owned. |
| MCP HTTP host | Trusted deployment boundary | Must provide truthful headers/TLS state, emit owned response headers before cleanup, bind local services to loopback, and enforce resolver/egress policy. |
| MCP input handlers and extensions | Trusted application policy over untrusted JSON | MRTR requests and responses are schema-checked, but the callback decides whether elicitation, roots, or sampling is authorized. Extension settings remain application-defined. |
| Provider-managed files | Provider-scoped handles | A provider guard is checked before network I/O; do not reuse opaque handles across providers. |
| Persisted history and paused state | Untrusted input | Versioned parsers and allocation limits apply. Store it with application-appropriate access control and retention. |
| Custom transports, hooks, tools, and exporters | Trusted application code | They receive the data required for their job and are responsible for further disclosure and side-effect policy. |
ZigAI does not persist API keys. Applications should keep credentials outside message history, metadata, tool results, paused state, and cassette fixtures.
Agent specifications cannot contain literal API keys. A structured
api_key.env reference is read only when its name appears in
EnvironmentPolicy.secret_names, and an empty or missing value is rejected.
String interpolation is separate, field-specific, disabled by default, and
restricted to interpolation_names. Dry-run provider callbacks receive only a
has_api_key bit; credential bytes are passed exclusively to model
construction. Treat the specification CLI's --allow-env flags as explicit
authority to read those process variables.