Skip to content

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.

A caps.yaml document is a YAML list. Each entry is one capability:

- name: github-api
type: http
allow: [ ... ]
transforms: [ ... ]
budgets: { ... }
FieldTypeRequiredDescription
namestringyesUnique 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.
typestringyesClosed enum: http (only value today; reserved for future protocols).
allowAllowRule[]yesOne or more rules that admit requests. At least one required.
transformsTransformRule[]noRequest mutations (auth, headers, path, query, body) that fire on allowed requests.
budgetsBudgets objectnoQuota counters that enforce usage ceilings.

One entry under allow. Declares static match criteria plus optional CEL predicates and request-body schema.

FieldTypeRequiredDescription
namestringnoOptional identifier so transforms can target this rule via applies_to. Identifier rules apply. Anonymous rules are valid but not referenceable.
domainsstring listyesHostname patterns. Globs allowed (*.example.com). Each entry is normalized (lowercased, trailing dot stripped, default port dropped).
methodsstring listyesHTTP methods (uppercase). Closed enum: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.
pathsstring listnoPath glob patterns (/api/v1/**, /webhook/*). Must start with /. Empty means any path.
allow_insecureboolnoDefault false (HTTPS required). Set true to admit non-HTTPS to this rule.
matchAllowRuleMatchnoCEL predicate ANDed with the static criteria.
request_formatstringconditionalBody 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_schemaJSON SchemanoJSON 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.
FieldTypeRequiredDescription
exprCELnoBoolean CEL expression. ANDed with domain/method/path checks.
requires_bodyboolnotrue makes expr evaluate at the body phase with request.body in scope. false (default) evaluates at admission with headers / query / method / path only.

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).

Exactly one of {header, path, query, body, auth} per transform. All other shapes within a single rule are mutually exclusive.

FieldTypeRequiredDescription
namestringnoOptional identifier for audit attribution.
directionstringnoClosed enum: "" (resolves to request) or "request". response is rejected at save (reserved for future use).
applies_tostring listnoNames of allow rules this transform fires for. Each entry must match a named rule. Empty (default) means all rules.
requires_bodyboolnoFor header / path / query variants: enables request.body in the CEL env. Body variants set this implicitly. Rejected on auth variants.
headerHeaderTransformnoHeader mutation.
pathPathTransformnoPath rewrite.
queryQueryTransformnoQuery-string mutation.
bodyBodyTransformnoJSON body mutation.
authAuthTransformnoCredential injection.
FieldTypeRequiredDescription
namestringyesHeader name (RFC 7230 token).
actionstringyesClosed enum: set (replace or create), add (add allowing multiples), remove (delete).
valueCELconditionalCEL 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.

FieldTypeRequiredDescription
actionstringyesClosed enum: replace (regex match + substitute) or prefix (prepend literal).
patternRE2conditionalGo RE2 regex. Required for replace, rejected for prefix.
valueCELyesCEL 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.

FieldTypeRequiredDescription
actionstringyesClosed enum: set or remove.
keystringyesParameter name.
valueCELconditionalCEL expression producing a string. Required for set, rejected for remove.

URL-encoding is applied automatically.

FieldTypeRequiredDescription
actionstringyesClosed enum: merge (deep-merge a map into the body) or prefix (prepend a string to a named field).
keystringconditionalField name (for prefix only). Required for prefix, rejected for merge.
valueCELyesFor merge: produces map<string, dyn>. For prefix: produces string.

All rules targeted by a body transform must declare request_format: "application/json".

FieldTypeRequiredDescription
credentialstringyesName of an org-scoped credential (matches what you created via POST /v1/credentials).
providerstringyesProvider 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:
counters:
- name: ...
...
FieldTypeRequiredDescription
countersBudgetCounter[]noZero or more counters.

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.

FieldTypeRequired (general)Description
namestringyesIdentifier unique within the capability. No leading underscore (reserved for platform-internal counters).
sourcestringyesClosed enum: request.count, request.bytes, response.bytes, response.
valueCELsource-dependentOnly valid (and required) for source: response.
requires_bodyboolsource-dependentOnly valid for source: response. true evaluates at the response body phase; false at the response headers phase.
framingstringconditionalResponse framing: "", single_document, or sse. Required when source: response + requires_body: true.
formatstringconditionalBody format: "" or application/json. Required alongside framing.
encodingstringconditionalBody encoding: "" or identity. Required alongside framing.
sse_eventstringnoNarrows extraction to a named SSE event type. Only valid with framing: sse + requires_body: true.
status_codesstring listsource-dependentResponse 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.
accumulatorstringconditionalHow per-event values fold: max (cumulative providers), sum (per-event deltas), last (replacement). Required for source: response — no default.
reserve_per_requestint64noQuota reserved at allow time for streaming responses. Non-negative. Rejected on synchronous sources.
failure_chargeint64noMinimum debit on stream termination without a clean terminator. Non-negative, ≤ scope.max. Rejected on synchronous sources.
max_concurrentintnoMax in-flight requests holding a reservation. Default 0 (unlimited).
workspaceBudgetScopenoWorkspace-level ceiling and TTL.
taskBudgetScopenoTask-level ceiling and TTL.
terminal_event_typestringconditionalSSE terminal event type (e.g. message_stop). Required for framing: sse + requires_body: true. Exactly one terminator per counter.
terminal_sentinelstringnoString literal marking stream end. At most one terminator per counter.
terminal_celCELnoCEL bool over SSE event marking termination. At most one terminator per counter.
sourceRequired fieldsRejected 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.bytesstatus_codesvalue, requires_body, sse_event, framing, format, encoding, terminators
response (headers phase)value, status_codes, accumulator, requires_body: falseframing, 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
FieldTypeRequiredDescription
maxint64noQuota limit. 0 means unlimited. Non-negative.
ttlGo durationnoReset 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.

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

FieldTypeNotes
request.methodstringHTTP verb (uppercase).
request.pathstringURI path component, no query string.
request.hoststringRequest host (lowercase).
request.schemestringhttp or https.
request.headersmap<string, string>Header names lowercased; for headers repeated on the wire, the proxy joins values with , per RFC 7230.
request.raw_querystringURL-encoded query string as it appears on the wire (no leading ?).
request.querymap<string, list<string>>Parsed raw_query. Each key maps to one or more decoded values.
request.bodydyn or schema-typed objectAvailable 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.
FieldTypeNotes
response.statusintHTTP status.
response.headersmap<string, string>Lowercased keys, comma-joined values.
response.sizeintBytes observed on the wire.
response.bodydynAvailable only for single_document body counters. SSE counters and header-only counters reject response.body at save time.

Available in admission and transform envs. Not declared in counter envs.

FieldTypeNotes
workspace.idstringWorkspace identifier.
workspace.org_idstringTenant identifier.
workspace.rolestringAgent-role name (slug).
workspace.environmentstringRole’s environment label.

Available in admission and transform envs. Not declared in counter envs.

FieldTypeNotes
task.idstringTask identifier (when scoped).

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.

Phaserequestresponseworkspace / tasksecretseventrequest.body
AllowRule match (admission, no body)
AllowRule match (admission, body)
Header transform valuewith schema
Query transform valuewith schema
Path transform valuewith schema
Body transform value
Counter value (header-only)
Counter value (single_document body)✓ + body
Counter value (SSE)
Counter terminal_cel
  • capability.typehttp
  • allow.methodsGET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
  • allow.request_format"", application/json
  • transform.direction"", request
  • header.actionset, add, remove
  • path.actionreplace, prefix
  • query.actionset, remove
  • body.actionmerge, prefix
  • counter.sourcerequest.count, request.bytes, response.bytes, response
  • counter.accumulatormax, sum, last
  • counter.framing"", single_document, sse
  • counter.format"", application/json
  • counter.encoding"", identity

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 name on 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 bare example).
    • 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 (:80 for http, :443 for https), trailing dot stripped.
  • 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].

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, and max_concurrent are 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_schema declared with body-less methods — if a rule with request_schema set has methods that include GET or HEAD, those requests can’t evaluate against the schema (no body) and will fall through to siblings or no_match. Split into separate allow rules when both shapes need to admit.
  • Body-carrying method without request_schemaPOST / PUT / PATCH rules 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.