caps.yaml reference
A caps.yaml document declares which outbound network calls a
workspace’s agent is allowed to make, what gets rewritten on the
way out (auth injection, header/body mutations), and how usage is
counted against per-workspace / per-task quotas. The proxy
enforces these rules transparently — the agent makes its normal
HTTP calls and gets denied or transformed by policy, no code
changes required.
This page is the reference catalog for the YAML schema. For the conceptual model — what a capability is and why it exists — see the rest of the docs.
Document shape
Section titled “Document shape”A caps.yaml document is a YAML list. Each entry is one capability:
- name: github-api type: http allow: [ ... ] transforms: [ ... ] budgets: { ... }Capability
Section titled “Capability”| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Unique identifier within the document. Must match [A-Za-z][A-Za-z0-9_-]{0,63} — letters / digits / underscore / dash, letter-prefixed, 1-64 chars. Leading underscore is reserved for platform use. |
type | string | yes | Closed enum: http (only value today; reserved for future protocols). |
allow | AllowRule[] | yes | One or more rules that admit requests. At least one required. |
transforms | TransformRule[] | no | Request mutations (auth, headers, path, query, body) that fire on allowed requests. |
budgets | Budgets object | no | Quota counters that enforce usage ceilings. |
AllowRule
Section titled “AllowRule”One entry under allow. Declares static match criteria plus
optional CEL predicates and request-body schema.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | Optional identifier so transforms can target this rule via applies_to. Identifier rules apply. Anonymous rules are valid but not referenceable. |
domains | string list | yes | Hostname patterns. Globs allowed (*.example.com). Each entry is normalized (lowercased, trailing dot stripped, default port dropped). |
methods | string list | yes | HTTP methods (uppercase). Closed enum: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS. |
paths | string list | no | Path glob patterns (/api/v1/**, /webhook/*). Must start with /. Empty means any path. |
allow_insecure | bool | no | Default false (HTTPS required). Set true to admit non-HTTPS to this rule. |
match | AllowRuleMatch | no | CEL predicate ANDed with the static criteria. |
request_format | string | conditional | Body content type. Closed enum: "" (no body parsing) or "application/json". Required when any body-inspecting feature is present (schema, body-matching CEL, body transforms). |
request_schema | JSON Schema | no | JSON Schema validating the request body. Requires request_format: "application/json". Compiled with JSON Schema Draft 2020-12 as the default draft — set $schema explicitly to pick a different one. Internal $ref (e.g. "#/$defs/Foo") works; external $ref is rejected so a referenced URL can’t be loaded over the network. Authored inline as YAML (the natural form inside caps.yaml) or as a JSON literal — the YAML parser accepts either, since JSON is a YAML subset. |
AllowRuleMatch
Section titled “AllowRuleMatch”| Field | Type | Required | Description |
|---|---|---|---|
expr | CEL | no | Boolean CEL expression. ANDed with domain/method/path checks. |
requires_body | bool | no | true makes expr evaluate at the body phase with request.body in scope. false (default) evaluates at admission with headers / query / method / path only. |
CEL environment
Section titled “CEL environment”See CEL environments for the full table. The
admission env declares request.*, workspace.*, task.* — no
secrets, no response, no event. request.body is only
available when requires_body: true (and is rejected on bodyless
predicates at save time).
TransformRule
Section titled “TransformRule”Exactly one of {header, path, query, body, auth} per transform.
All other shapes within a single rule are mutually exclusive.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | Optional identifier for audit attribution. |
direction | string | no | Closed enum: "" (resolves to request) or "request". response is rejected at save (reserved for future use). |
applies_to | string list | no | Names of allow rules this transform fires for. Each entry must match a named rule. Empty (default) means all rules. |
requires_body | bool | no | For header / path / query variants: enables request.body in the CEL env. Body variants set this implicitly. Rejected on auth variants. |
header | HeaderTransform | no | Header mutation. |
path | PathTransform | no | Path rewrite. |
query | QueryTransform | no | Query-string mutation. |
body | BodyTransform | no | JSON body mutation. |
auth | AuthTransform | no | Credential injection. |
HeaderTransform
Section titled “HeaderTransform”| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Header name (RFC 7230 token). |
action | string | yes | Closed enum: set (replace or create), add (add allowing multiples), remove (delete). |
value | CEL | conditional | CEL expression producing a string. Required for set / add, rejected for remove. |
Secrets via secrets["name"] are supported in the value. Dynamic
keys (variable lookups into secrets) are rejected.
PathTransform
Section titled “PathTransform”| Field | Type | Required | Description |
|---|---|---|---|
action | string | yes | Closed enum: replace (regex match + substitute) or prefix (prepend literal). |
pattern | RE2 | conditional | Go RE2 regex. Required for replace, rejected for prefix. |
value | CEL | yes | CEL expression producing a string. For replace, this is the substitution (no backrefs). For prefix, this is the prepended text. |
The path-transform CEL env has no secrets — path values land in
access logs, so credential references reject at save time. With
requires_body: true plus a request_schema on the matched rule,
request.body is available. See CEL environments.
QueryTransform
Section titled “QueryTransform”| Field | Type | Required | Description |
|---|---|---|---|
action | string | yes | Closed enum: set or remove. |
key | string | yes | Parameter name. |
value | CEL | conditional | CEL expression producing a string. Required for set, rejected for remove. |
URL-encoding is applied automatically.
BodyTransform
Section titled “BodyTransform”| Field | Type | Required | Description |
|---|---|---|---|
action | string | yes | Closed enum: merge (deep-merge a map into the body) or prefix (prepend a string to a named field). |
key | string | conditional | Field name (for prefix only). Required for prefix, rejected for merge. |
value | CEL | yes | For merge: produces map<string, dyn>. For prefix: produces string. |
All rules targeted by a body transform must declare
request_format: "application/json".
AuthTransform
Section titled “AuthTransform”| Field | Type | Required | Description |
|---|---|---|---|
credential | string | yes | Name of an org-scoped credential (matches what you created via POST /v1/credentials). |
provider | string | yes | Provider slug — closed enum validated against the known auth providers. OAuth families: google, microsoft. Static-token APIs: anthropic, openai, gemini, azure_openai, slack_bot, github_pat, notion_token, linear_pat, hubspot_pat, discord_bot, gitlab_token, sendgrid, pagerduty, grafana, databricks, nvd, elevenlabs, airtable, splunk, newrelic, virustotal, elasticsearch. Basic-auth: jira, confluence, gitlab_git_https, github_git_https. Query-API-key: google_search. |
Token format (header vs query vs custom sink) is fixed per-provider by the proxy — no CEL involved. The minted token never appears in audit detail.
Budgets
Section titled “Budgets”budgets: counters: - name: ... ...| Field | Type | Required | Description |
|---|---|---|---|
counters | BudgetCounter[] | no | Zero or more counters. |
BudgetCounter
Section titled “BudgetCounter”One counter on a capability. The full validation table depends on
source — see Source-dependent fields
below.
Accept-Encoding requirement. Any capability that declares a
body-reading response counter (source: response.bytes, or
source: response with requires_body: true) must also declare a
header transform that pins Accept-Encoding: identity:
transforms: - header: name: Accept-Encoding action: set value: '"identity"'Without it the upstream may compress the response and the runtime extractor would see encoded bytes; the parser hard-rejects at save time rather than letting the counter silently produce wrong debits.
| Field | Type | Required (general) | Description |
|---|---|---|---|
name | string | yes | Identifier unique within the capability. No leading underscore (reserved for platform-internal counters). |
source | string | yes | Closed enum: request.count, request.bytes, response.bytes, response. |
value | CEL | source-dependent | Only valid (and required) for source: response. |
requires_body | bool | source-dependent | Only valid for source: response. true evaluates at the response body phase; false at the response headers phase. |
framing | string | conditional | Response framing: "", single_document, or sse. Required when source: response + requires_body: true. |
format | string | conditional | Body format: "" or application/json. Required alongside framing. |
encoding | string | conditional | Body encoding: "" or identity. Required alongside framing. |
sse_event | string | no | Narrows extraction to a named SSE event type. Only valid with framing: sse + requires_body: true. |
status_codes | string list | source-dependent | Response HTTP status patterns: exact ("200"), range ("200-299"), family ("2xx"). Codes must fall in [100, 599]; families cover 1xx through 5xx. Required for source: response and source: response.bytes. |
accumulator | string | conditional | How per-event values fold: max (cumulative providers), sum (per-event deltas), last (replacement). Required for source: response — no default. |
reserve_per_request | int64 | no | Quota reserved at allow time for streaming responses. Non-negative. Rejected on synchronous sources. |
failure_charge | int64 | no | Minimum debit on stream termination without a clean terminator. Non-negative, ≤ scope.max. Rejected on synchronous sources. |
max_concurrent | int | no | Max in-flight requests holding a reservation. Default 0 (unlimited). |
workspace | BudgetScope | no | Workspace-level ceiling and TTL. |
task | BudgetScope | no | Task-level ceiling and TTL. |
terminal_event_type | string | conditional | SSE terminal event type (e.g. message_stop). Required for framing: sse + requires_body: true. Exactly one terminator per counter. |
terminal_sentinel | string | no | String literal marking stream end. At most one terminator per counter. |
terminal_cel | CEL | no | CEL bool over SSE event marking termination. At most one terminator per counter. |
Source-dependent fields
Section titled “Source-dependent fields”source | Required fields | Rejected fields |
|---|---|---|
request.count | (just name + source) | value, requires_body, sse_event, reserve_per_request, failure_charge, framing, format, encoding, terminators |
request.bytes | (just name + source) | Same as request.count |
response.bytes | status_codes | value, requires_body, sse_event, framing, format, encoding, terminators |
response (headers phase) | value, status_codes, accumulator, requires_body: false | framing, format, encoding, sse_event, terminators |
response (body phase) | value, status_codes, accumulator, requires_body: true, framing, format, encoding; if framing: sse, exactly one of terminal_event_type / terminal_sentinel / terminal_cel | — |
BudgetScope
Section titled “BudgetScope”| Field | Type | Required | Description |
|---|---|---|---|
max | int64 | no | Quota limit. 0 means unlimited. Non-negative. |
ttl | Go duration | no | Reset interval (e.g. 5m, 1h, 24h). Empty means never resets. Must be positive when set. |
Cross-counter rule: if both workspace and task are set,
task.max ≤ workspace.max and task.ttl ≤ workspace.ttl.
CEL environments
Section titled “CEL environments”Different lifecycle phases expose different variables. A CEL
expression that references a variable not declared by its phase’s
env is rejected at save time with undeclared reference: <name>.
request (admission + transforms)
Section titled “request (admission + transforms)”| Field | Type | Notes |
|---|---|---|
request.method | string | HTTP verb (uppercase). |
request.path | string | URI path component, no query string. |
request.host | string | Request host (lowercase). |
request.scheme | string | http or https. |
request.headers | map<string, string> | Header names lowercased; for headers repeated on the wire, the proxy joins values with , per RFC 7230. |
request.raw_query | string | URL-encoded query string as it appears on the wire (no leading ?). |
request.query | map<string, list<string>> | Parsed raw_query. Each key maps to one or more decoded values. |
request.body | dyn or schema-typed object | Available only when the phase opts into body — admission with requires_body: true, body transforms, or header / query / path transforms with requires_body: true on a rule with request_schema. |
response (counters)
Section titled “response (counters)”| Field | Type | Notes |
|---|---|---|
response.status | int | HTTP status. |
response.headers | map<string, string> | Lowercased keys, comma-joined values. |
response.size | int | Bytes observed on the wire. |
response.body | dyn | Available only for single_document body counters. SSE counters and header-only counters reject response.body at save time. |
workspace
Section titled “workspace”Available in admission and transform envs. Not declared in counter envs.
| Field | Type | Notes |
|---|---|---|
workspace.id | string | Workspace identifier. |
workspace.org_id | string | Tenant identifier. |
workspace.role | string | Agent-role name (slug). |
workspace.environment | string | Role’s environment label. |
Available in admission and transform envs. Not declared in counter envs.
| Field | Type | Notes |
|---|---|---|
task.id | string | Task identifier (when scoped). |
secrets
Section titled “secrets”map<string, string>. Available in header / query / body
transforms only. Path transforms and admission predicates and
counters reject secrets at save time. Keys must be string
literals — secrets[var_name] is rejected.
map<string, dyn>. Available in SSE counter value expressions
and capability-level terminal_cel. The shape is whatever the
SSE event payload deserializes to.
Env by phase
Section titled “Env by phase”| Phase | request | response | workspace / task | secrets | event | request.body |
|---|---|---|---|---|---|---|
| AllowRule match (admission, no body) | ✓ | — | ✓ | — | — | — |
| AllowRule match (admission, body) | ✓ | — | ✓ | — | — | ✓ |
Header transform value | ✓ | — | ✓ | ✓ | — | with schema |
Query transform value | ✓ | — | ✓ | ✓ | — | with schema |
Path transform value | ✓ | — | ✓ | — | — | with schema |
Body transform value | ✓ | — | ✓ | ✓ | — | ✓ |
Counter value (header-only) | — | ✓ | — | — | — | — |
Counter value (single_document body) | — | ✓ + body | — | — | — | — |
Counter value (SSE) | — | ✓ | — | — | ✓ | — |
Counter terminal_cel | — | ✓ | — | — | ✓ | — |
Closed enums (quick reference)
Section titled “Closed enums (quick reference)”capability.type—httpallow.methods—GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONSallow.request_format—"",application/jsontransform.direction—"",requestheader.action—set,add,removepath.action—replace,prefixquery.action—set,removebody.action—merge,prefixcounter.source—request.count,request.bytes,response.bytes,responsecounter.accumulator—max,sum,lastcounter.framing—"",single_document,ssecounter.format—"",application/jsoncounter.encoding—"",identity
A complete example
Section titled “A complete example”A capability for Anthropic’s /v1/messages endpoint that injects
an API key, schema-validates the request body, mutates a metadata
field, and tracks request count + per-token usage:
- name: anthropic-api type: http allow: - name: messages domains: [api.anthropic.com] methods: [POST] paths: [/v1/messages] request_format: application/json request_schema: type: object properties: model: { type: string } messages: type: array items: type: object properties: role: { type: string } content: { type: string } max_tokens: { type: integer } system: { type: string } required: [model, messages, max_tokens] match: expr: 'request.body.model.startsWith("claude-")' requires_body: true - name: list-models domains: [api.anthropic.com] methods: [GET] paths: [/v1/models] transforms: - name: inject-api-key auth: credential: anthropic-api-key provider: anthropic - name: pin-identity-encoding # Required because output_tokens is a body-reading response # counter — without this the upstream may gzip the SSE stream # and extraction would see encoded bytes. header: name: Accept-Encoding action: set value: '"identity"' - name: tag-workspace applies_to: [messages] body: action: merge value: | { "metadata": { "workspace_id": workspace.id } } budgets: counters: - name: requests source: request.count workspace: { max: 1000, ttl: "1h" } task: { max: 100, ttl: "5m" } - name: output_tokens source: response requires_body: true framing: sse format: application/json encoding: identity sse_event: message_delta terminal_event_type: message_stop status_codes: ["2xx"] accumulator: sum value: 'int(event.usage.output_tokens)' reserve_per_request: 5000 failure_charge: 1000 workspace: { max: 1000000, ttl: "24h" }- Identifier rules apply to
nameon capabilities, allow rules, transforms, counters, and secret names (secrets["..."]keys):[A-Za-z][A-Za-z0-9_-]{0,63}— letters (mixed case), digits, underscore, dash. Letter-prefixed, 1-64 chars. Leading underscore is rejected; the platform uses_-prefixed names internally for synthetic capabilities and counters that bypass the user-facing validator. - CEL expressions compile at save time. Type errors surface
immediately on
POST /v1/agent-roles(or wherever the caps is attached). Runtime errors during evaluation are floored / logged rather than silently passing. - Secrets references (
secrets["name"]) resolve at proxy activation, not parse time. The name must be a literal — variable lookups are rejected. - Domain validation runs at parse time and enforces:
- Hostnames must have ≥2 labels (e.g.
example.com, not bareexample). - Wildcards: single
*per label allowed (*.example.com);**rejected; bare*rejected (must include a TLD). - IPv4 addresses are accepted as-is (
10.0.0.1). - IPv6 addresses must be bracketed (
[::1],[::1]:8080). - Schemes (
http://,https://) are rejected — pass just the host. - Each entry is normalized: whitespace trimmed, lowercased,
default port stripped (
:80for http,:443for https), trailing dot stripped.
- Hostnames must have ≥2 labels (e.g.
- Path patterns use shell-style globs:
*matches within a segment,**matches across segments. Paths must start with/. - Status code patterns support exact (
"200"), range ("200-299"), and family ("2xx"). Codes must fall in[100, 599].
Save-time warnings
Section titled “Save-time warnings”These don’t reject the save but surface as stderr lines on
evershell caps validate (and as a warnings list returned by
POST /v1/agent-roles):
- Counter with no enforcement — a counter whose
workspace,task, andmax_concurrentare all unset is an observation-only counter: it emits debit measurements at its phase (dashboards, cost tracking) but enforces no ceiling. Legitimate for post-hoc metering; the warning surfaces the configuration so it’s visible at review time. request_schemadeclared with body-less methods — if a rule withrequest_schemaset hasmethodsthat includeGETorHEAD, those requests can’t evaluate against the schema (no body) and will fall through to siblings orno_match. Split into separate allow rules when both shapes need to admit.- Body-carrying method without
request_schema—POST/PUT/PATCHrules without a declared schema can’t type-check body fields in transforms. Fine for pass-through capabilities; worth surfacing for security review. - Overlapping allow rules — when two rules across one or more
capabilities admit the same
(domain, method, path)triple, the compiled Rego uses a lex-first deterministic picker but the ambiguity is worth resolving at authorship.