fork of Kludex/zigai, ported to zig 0.17
zigai docs security.md
7.9 kB · 134 lines
Markdown
at main

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:

  • protectedResourceDiscoveryUrls returns the endpoint-path and root RFC 9728 URLs in required order; parseProtectedResourceMetadata accepts extensions but requires the canonical resource, at least one distinct issuer, and the Bearer header method;
  • authorizationServerDiscoveryUrls returns RFC 8414 and OIDC URLs in required path-aware order; parsed metadata must match the selected issuer exactly, use eligible HTTPS endpoints, and advertise S256 PKCE;
  • ClientPolicy binds one canonical resource and authorization-server issuer to a TokenProvider. The callback receives the RFC 8707 resource, method, accumulated scopes, and refresh reason. Return tokens with AccessToken.initAlloc so partial allocation failures are cleaned safely;
  • Streamable HTTP adds Authorization: Bearer ... to every POST. A valid 401 challenge can request a bounded refresh; a 403 insufficient_scope challenge can request a bounded stable-order scope union. Static Authorization headers cannot be combined with a token policy;
  • ServerPolicy passes only the token value, canonical audience, method, and params to the application verifier. The verifier must validate signature, expiry, issuer, and audience before returning authorized. Denials produce an owned WWW-Authenticate response 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.