Source format support
Check support at four stages: parsing, local source capture, lowering into AIR, and execution of the resulting operation. Passing one stage does not establish support for the next.
For example, proto3 parsing can produce aligned artifacts while execution still needs an HTTP annotation or a declared JSON transcoder. Use the tables below to identify the adapter, required configuration, and refusal boundary.
Support matrix
Section titled “Support matrix”| Source | Accepted input | Local source graph | Generated runtime | Declined or external behavior |
|---|---|---|---|---|
| OpenAPI | OpenAPI 3.x YAML or JSON | Captures and resolves local $ref files | HTTP with JSON | Remote references are recorded but not fetched |
| Swagger | Swagger 2.0 YAML or JSON | Captures and resolves local $ref files | Converted to OpenAPI, then HTTP with JSON | Remote references are recorded but not fetched |
| GraphQL | GraphQL SDL (.graphql, .gql, .graphqls) | Composes every SDL file in the snapshot | Queries, mutations, and subscriptions as bounded windows | The generated SDKs refuse the subscription wire by design |
| gRPC | proto3 (.proto) | Captures transitive local imports | The route a google.api.http method declares; otherwise HTTP-shaped calls through a declared JSON transcoder | Native gRPC and streaming RPCs are refused |
| SOAP | WSDL 1.1 with embedded or local XSD | Captures wsdl:import, xsd:include, and xsd:import | Supported document/literal bindings | See the test-backed wire protocol matrix |
| Google APIs | Discovery restDescription JSON | One document | HTTP with JSON | Does not call Google discovery services |
| OData | v2 or v4 $metadata / EDMX XML | One metadata entrypoint | Entity sets; actions, functions, and v2 function imports; instance-bound operations through their entity set | Collection-bound and ambiguously-bound operations decline with a named reason; navigation semantics require review |
| Postman | Collection v2.0 or v2.1 JSON | One collection document | HTTP with JSON | Pre-request and test scripts are reported but never executed |
| Captured traffic (HAR) | HTTP Archive 1.2 JSON (.har) | One capture document | HTTP with JSON | See Captured traffic (HAR) — every operation compiles review_required at most; none is ever approved from a capture |
The format adapter is only the first stage. Every source then uses the same normalize, classify, manifest, validate, capability, and generation pipeline.
Detect the source before compiling
Section titled “Detect the source before compiling”For a new input, use the discovery flow:
anvil agentify path/to/spec --out generated/serviceagentify captures the source, compiles it, assesses operation readiness, and
proposes capability groupings. It stops for review.
For a source directory or multiple explicit entrypoints, capture first:
anvil source add path/to/spec-directoryanvil source listanvil source show <snapshot-id>anvil source validate <snapshot-id>Then compile one locked entrypoint:
anvil compile --source <snapshot-id> \ --entrypoint path/inside/snapshot \ --out generated/serviceThe snapshot contains the exact local bytes Anvil read. Its id is content derived. A broken or unclassified input may still be captured for diagnosis, but only a valid snapshot can be compiled.
Local and remote references
Section titled “Local and remote references”Source capture follows local references that belong to the supplied source graph. It rejects path traversal and never follows a local reference outside the import root.
Remote references are not fetched. This keeps compilation reproducible and prevents a URL from changing the source after review. Vendor or remote schemas must be vendored into the import root, or resolved upstream into a contract you can snapshot.
For multi-entrypoint directories, pass the exact entrypoint when compiling a snapshot. Do not rely on filename order.
Format-specific notes
Section titled “Format-specific notes”OpenAPI and Swagger
Section titled “OpenAPI and Swagger”OpenAPI is the richest source for HTTP semantics, but the document still may not prove business effect or idempotency. Inspect POST operations carefully; method alone is not a sufficient safety classification.
Swagger 2.0 is upgraded before normalization. It may span files exactly as
OpenAPI 3.x may: a 2.0 entrypoint’s external $refs are folded into the
document before conversion, because the 2.0 converter cannot follow them
itself. Only external references are folded in — an internal #/definitions
reference is left as written, so a self-referential definition stays a
reference rather than becoming a cycle. A reference to a file the snapshot does
not carry fails the compile rather than silently dropping the definition.
Review conversion diagnostics, especially around body/form parameters, security definitions, and response schemas.
GraphQL SDL
Section titled “GraphQL SDL”Anvil maps each root field to one operation:
Queryfields are reads;Mutationfields are mutations; andSubscriptionfields become bounded observation windows: the call collects events until an event or time bound and returns them as an array. See the wire protocol matrix for the contract and its bounds.
A subscription’s window defaults to 100 events or 30 seconds, whichever comes first. An operator can resize either ceiling in the manifest:
operations: orderUpdated: stream: max_events: 500 max_seconds: 120The absolute caps — 10,000 events, 300 seconds — live on AIR’s own schema, so neither a manifest nor a hand-edited document can represent an unbounded window. A manifest can resize a window but never create one: which operations stream is a fact the compiler reads from the SDL, not a declaration.
Arguments become the request schema and return types become response schemas. Custom scalars degrade to documented string values unless the source provides a stronger mapping. SDL does not carry endpoint URL, resolver-side authorization, or mutation idempotency; supply those operational facts separately.
A schema split across files compiles as one schema. SDL has no import
statement, so composition is concatenation: every SDL document in the snapshot
is composed, entrypoint first, and extend type Query blocks in sibling files
add their fields to the root type. Capture the directory to get them all:
anvil source add path/to/graphql-schema-directoryanvil compile --source <snapshot-id> --entrypoint schema.graphql --out generated/servicePointing anvil compile at a single .graphql file compiles that file alone,
which is the right answer for a single-file schema and the wrong one for a
split schema whose root type would come out nearly empty.
gRPC and proto3
Section titled “gRPC and proto3”Each RPC becomes an operation. Request and response messages become JSON
schemas; nested messages, enums, repeated fields, maps, oneof, and local
imports are parsed through protobufjs. Unresolved well-known types degrade
conservatively instead of stopping the whole compile.
A method carrying google.api.http is lowered to the verb and path it declares,
with path, query, and body bound as the rule says. A trailing custom method
(/v1/items/{item_id}:adjust, AIP-136) names the operation’s action, so it does
not collide with the collection’s plain POST. Such a method classifies exactly
as the equivalent OpenAPI operation, verb semantics included.
For a method with no annotation, names do not prove side effects. Read-like
prefixes such as Get and List can classify as reads; other RPCs remain
mutations until reviewed. The adapter preserves service and method identity, but
the generated HTTP-shaped request model is not itself a binary gRPC client.
Validate the runtime bridge or protocol adapter used in your deployment.
See the wire protocol matrix for which annotation shapes are refused and why.
SOAP and WSDL
Section titled “SOAP and WSDL”Each WSDL portType operation becomes a POST-shaped operation. The adapter
understands the common document/literal XSD subset:
- global elements;
complexTypewithsequenceorall;simpleTypeenumeration restrictions;complexContentextension;- element references;
minOccursandmaxOccurs; and- common XSD scalar types.
Operation names provide conservative effect hints; they do not prove idempotency. See Add the safety facts a WSDL leaves out for a complete example.
Google Discovery
Section titled “Google Discovery”Discovery resources and methods map mechanically into paths and operations. Request and response references become component schemas, and published OAuth scopes are preserved. Operational authorization, quotas, and tenant policy remain deployment concerns.
OData metadata
Section titled “OData metadata”Each entity set becomes list, read-one, create, update, and delete operations
where the metadata permits them. The adapter recognizes the structural subset
shared by OData v2 and v4 and honors supported SAP
sap:creatable, sap:updatable, and sap:deletable annotations.
An update body omits the entity’s key properties. The key is how PATCH /Set('{key}') addresses the entity, so carrying it in the body as well would
give a caller two places to put one identity.
Entity sets are only the nouns. A service’s verbs — ActivateProduct,
GetNearestAirport, ResetDataSource — are compiled too, and OData states
their effect rather than leaving it to be guessed:
| Declaration | Lowers to | Effect |
|---|---|---|
v4 Function (via FunctionImport) | GET /Name(arg='value') | Read — v4 requires a function to be side-effect-free |
v4 Action (via ActionImport) | POST /Name with a JSON body | Mutation |
v2 FunctionImport with m:HttpMethod="GET" | GET /Name?arg='value' | Read |
v2 FunctionImport with m:HttpMethod="POST" | POST /Name?arg='value' | Mutation |
v2 FunctionImport with no m:HttpMethod | POST /Name | Mutation, with a diagnostic saying the document did not state it |
Parameter values carry OData’s literal syntax — a quoted string, v2’s
datetime'…' prefix, its M/L suffixes — compiled into the coordinate, so a
caller supplies the plain value and Anvil spells it correctly on the wire.
Bound actions and functions lower whenever their address can be constructed
without guessing. An operation bound to an entity instance is addressed through
the one entity set that exposes its binding type —
POST /Airports('{IcaoCode}')/Trippin.Deactivate — with the instance key as an
ordinary required path parameter (the same one the set’s own read-by-key asks
for), a bound function’s arguments inline in the coordinate, and a bound
action’s arguments as the JSON body. Three shapes still decline, each with a
diagnostic naming its reason: an operation bound to a collection, a binding
type no entity set exposes, and a binding type exposed by more than one set —
either address would be a guess, and Anvil does not put guesses on the wire.
Navigation properties, deep inserts, and service-specific conventions may still
need an enriched or upstream-normalized contract.
Postman collections
Section titled “Postman collections”Folders become tags, saved requests become operations, and saved response/body examples inform schemas. Secret-like auth values, header values, and query values are not copied into the model.
Anvil does not execute pre-request or test scripts, and never will: collections
routinely embed live tokens, and a script is arbitrary JavaScript running
inside a compiler. The refusal is loud rather than silent. Every script-bearing
request is named in a postman_script_untranslated compile diagnostic —
pre-request scripts are very often the auth flow itself, so a collection could
otherwise compile into a surface that looks complete and cannot authenticate.
The diagnostic carries the remedy: declare the auth contract in an Anvil
manifest, then prove the result against the live service with
anvil conformance.
Captured traffic (HAR)
Section titled “Captured traffic (HAR)”A HAR (HTTP Archive) 1.2 capture — a browser’s “Save all as HAR”, a proxy log,
a mitmproxy/Charles/Fiddler export — is the cheapest evidence that exists for
the long tail of internal and legacy HTTP APIs that have no spec, no Postman
collection, nothing declared at all. Anvil compiles one, but it never pretends
a capture is a contract: nobody declared what these endpoints mean, only what
crossed the wire once.
What is inferred. Entries are grouped by method and a templated path: a
path segment that is purely numeric, UUID-shaped, or otherwise contains a
digit is templated from a single sample already (the shape realistic ids
take — cus_101, order refs, UUIDs); the parameter is named from the
preceding literal segment’s singular (/customers/123 →
/customers/{customer_id}). A purely alphabetic segment is never templated
on cross-sample variation alone — collapsing two different resources that
happen to share a segment count into one operation would be a worse defect
than leaving them separate, so the adapter declines rather than guesses,
matching the same abstention discipline path grammar
classification uses. Query and header
parameters are the union of observed (non-secret) names, required only when
present in every sample. Request and response JSON bodies are schema-inferred
conservatively across every sample mapped to an operation: a property’s type
is the union of types observed for it, required lists only properties
present in every sample that carries a body at all, and
additionalProperties: true always — an observed shape is a lower bound,
never a closed contract. The auth scheme (Bearer, Basic, or an api-key
header/query carrier NAME) is recorded from what was actually presented;
real credential wiring (anvil enrich-sources init) still needs an operator.
What is dropped, before anything else runs. Authorization, Cookie,
Set-Cookie, Proxy-Authorization, and any header or query name matching
/token|secret|key|password|session/i are stripped first. Their VALUES are
never read for any purpose beyond one narrow exception — Authorization’s
leading scheme word (Bearer/Basic), never the credential after it — and
never copied into a schema, a diagnostic, an example, or a parameter. A
carrier’s NAME (e.g. the string X-Api-Key) is not secret material and is
kept, because it is what the compiled auth.provider.apiKey.name needs to be
useful. One har_secrets_dropped diagnostic reports how many values were
scrubbed, every time — a capture with nothing to drop still confirms the scrub
ran. Request/response BODY fields are not scrubbed by name (mirroring
Postman’s “body examples are documented payload data” policy); redacting a
capture’s body secrets before handing it to Anvil remains the operator’s
responsibility, the same as before handing anyone a HAR file at all.
Why nothing is ever approved from a capture. Every operation compiled from
a har source is capped at review_required — never generated, never
approved, regardless of how many samples support it. Every safety-relevant
claim (effect.kind, idempotency.mode, longRunning, confirmation.required,
retries.mode, auth.principal) is reattributed to AIR’s recorded_traffic
evidence kind and capped at 0.5 confidence: real reliability in what was
captured, but low confidence that one capture proves a general safety
semantic — a HAR has no way to prove a POST is idempotent, only that it was
called and something came back. A reviewNotes entry names exactly how many
captured requests backed the operation. A manifest can still enrich the
result as usual; nothing about the har posture blocks that path, only the
auto-approval one.
Path grammar classification
Section titled “Path grammar classification”Every source’s paths carry meaning in one of two grammars: nouns (REST — the
HTTP method is the verb) or verbs (RPC-over-HTTP — the terminal path segment is
the method). The compiler used to know this only implicitly, by source kind,
which is how Plaid — an RPC grammar published as OpenAPI — had 72% of its
operations take a bare CRUD verb (get, create) as their resource. The
grammar is now classified explicitly, once per compile, from one deterministic
pass over the estate’s operations (no network, no model), and declared in AIR
at service.source.pathGrammar with the classification, its basis, and the
counts that decided it — anvil inspect prints the verdict so a surprising
catalog name traces to an inspectable decision.
The taxonomy:
| Classification | Meaning | Example estate |
|---|---|---|
resource_grammar | Nouns in the path; HTTP methods carry the verb | Zendesk, GitHub, Stripe |
rpc_plain | Verbs as plain terminal segments; method mix collapsed onto POST | Plaid (POST /transactions/get) |
rpc_dotted | Dotted RPC method segments the URL itself declares | Slack (/chat.postMessage) |
adapter_lowered | A protocol adapter (WSDL/GraphQL/protobuf/MCP) wrote the paths; the shape is declared by construction | NetSuite’s lowered /NetSuitePortType/get |
ambiguous | The evidence genuinely splits; the compiler declines to pick | — |
The evidence is three paired signals — the fraction of operations ending in a
CRUD-verb segment, the GET/HEAD share of the method mix, and the fraction of
paths carrying a {param} segment — each with an RPC pole, a REST pole, and a
deliberate abstention band between them, plus a dotted-terminal count that
short-circuits to rpc_dotted (Slack: 174 of 174) and a verb-repetition count
recorded for the operator. A grammar is picked only when at least two signals
commit to one side and none commit to the other. Measured on the untrimmed
estates the thresholds were calibrated against: Plaid classifies rpc_plain
(252/351 verb terminals, 5/351 GET, 4/351 parameterized), Zendesk/GitHub/
Stripe/BigQuery classify resource_grammar, Slack rpc_dotted.
What the classification drives: whether the naming pass receives the
estate-wide path context that arms the trailing-method re-homing rules
(docs/design/resource-derivation-and-tool-name-stutter.md). Resource and
plain-RPC grammars read a trailing CRUD verb as a method; dotted-RPC and
adapter-lowered grammars must not, because there the method name is the
operation’s identity. An ambiguous estate emits a path_grammar_ambiguous
compile warning naming both candidate grammars and the counts for each, and
falls back to the source kind’s pre-classifier reading, so declining to guess
never changes an estate’s names. The warning names the remedy: a top-level
manifest declaration (path_grammar: rpc_plain — see
MANIFEST.md), which always
applies, and which records a path_grammar_override_contradicts_evidence
warning when it overrules a definite measured verdict.
Declined, and why
Section titled “Declined, and why”Some gaps on this page are decisions, not roadmap. Filing a permanent no under “not yet” reads as a promise, so each is stated with its reason.
- Native gRPC. A native call is length-prefixed protobuf over HTTP/2 with
the status in trailers. Anvil’s four generated SDKs are zero-dependency by
contract, and Python’s standard library has no HTTP/2 client — a native
client would either break that contract or exist in some languages and not
others, which is exactly the cross-surface divergence Anvil exists to
prevent. Two paths work and stay: a
google.api.httpannotation, which Anvil reads with nothing further declared, and a JSON transcoder (grpc-gateway, Envoy’s gRPC-JSON filter) declared as a protocol facade. - GraphQL subscriptions over WebSocket (
graphql-ws). The same wall: Python’s and Go’s standard libraries have no WebSocket client. Subscriptions are served overgraphql-sse— chunked HTTP, which every runtime already reads. A service that speaks onlygraphql-wsneeds an SSE-speaking gateway in front of it; that is a real cost, stated rather than hidden. - Postman script execution. Collections routinely embed live tokens, and a script is arbitrary JavaScript running inside a compiler. The scripts’ presence is surfaced per request (see above); the modeled fix is a manifest, never execution.
- Fetching remote
$refs at compile time. Compilation reads only the content-addressed snapshot, so a compile is reproducible and a URL cannot change a contract after review. Vendor remote schemas into the import root and snapshot them.
Gateways are a different entrypoint
Section titled “Gateways are a different entrypoint”An API gateway export is not automatically an API contract. Gateway artifacts may add routing, auth, policy, deployment revision, and ownership evidence that must remain attached to the imported API.
Run:
anvil estate support --jsonbefore preparing an export, then follow Gateway estates. Support varies by vendor and input tier; do not assume Anvil connects to a live gateway or understands every native bundle.
After compilation
Section titled “After compilation”For every format:
anvil status generated/serviceanvil inspect generated/serviceanvil assess generated/serviceanvil lint generated/serviceTreat adapter output as a candidate contract. Resolve missing business meaning in an Anvil manifest, recompile, and approve only the operations you have inspected.