Skip to content

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.

SourceAccepted inputLocal source graphGenerated runtimeDeclined or external behavior
OpenAPIOpenAPI 3.x YAML or JSONCaptures and resolves local $ref filesHTTP with JSONRemote references are recorded but not fetched
SwaggerSwagger 2.0 YAML or JSONCaptures and resolves local $ref filesConverted to OpenAPI, then HTTP with JSONRemote references are recorded but not fetched
GraphQLGraphQL SDL (.graphql, .gql, .graphqls)Composes every SDL file in the snapshotQueries, mutations, and subscriptions as bounded windowsThe generated SDKs refuse the subscription wire by design
gRPCproto3 (.proto)Captures transitive local importsThe route a google.api.http method declares; otherwise HTTP-shaped calls through a declared JSON transcoderNative gRPC and streaming RPCs are refused
SOAPWSDL 1.1 with embedded or local XSDCaptures wsdl:import, xsd:include, and xsd:importSupported document/literal bindingsSee the test-backed wire protocol matrix
Google APIsDiscovery restDescription JSONOne documentHTTP with JSONDoes not call Google discovery services
ODatav2 or v4 $metadata / EDMX XMLOne metadata entrypointEntity sets; actions, functions, and v2 function imports; instance-bound operations through their entity setCollection-bound and ambiguously-bound operations decline with a named reason; navigation semantics require review
PostmanCollection v2.0 or v2.1 JSONOne collection documentHTTP with JSONPre-request and test scripts are reported but never executed
Captured traffic (HAR)HTTP Archive 1.2 JSON (.har)One capture documentHTTP with JSONSee 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.

For a new input, use the discovery flow:

Terminal window
anvil agentify path/to/spec --out generated/service

agentify 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:

Terminal window
anvil source add path/to/spec-directory
anvil source list
anvil source show <snapshot-id>
anvil source validate <snapshot-id>

Then compile one locked entrypoint:

Terminal window
anvil compile --source <snapshot-id> \
--entrypoint path/inside/snapshot \
--out generated/service

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

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.

Compile a vendor’s whole published contract, not a trimmed copy, and choose the operations an agent sees with an exposure profile. The snapshot stays the contract of record. The profile narrows what may be approved and served, and the compile records both digests in service.source.profile.

jira.profile.yaml
profile: jira-issues # lowercase slug
description: Issue tracking for a support agent.
source: # optional pin to the reviewed contract
url: https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json
sha256: sha256:6ecc461b... # the downloaded file's bytes, as sha256sum prints them
select: # or: select: all
- operation_id: [getIssue, createIssue, editIssue, doTransition]
- tag: [Issue comments, Issue worklogs]
- tag: Projects
method: get
- path: "/rest/api/3/user/search/**"
exclude:
- method: delete
unexposed: skip # or: compile
schema_bounds: # optional; profile defaults shown
max_ref_depth: 3
max_schema_nodes: 2000
inherit_all_of: true
approve: # optional declarative approval
reviewed_by: reviewer@example.com
reason: Read operations reviewed against the vendor reference.
select:
- method: get
KeyMeaning
selectall, or a list of selectors. An operation is selected when any selector matches.
selectorAny of operation_id, tag, path, method, each a value or a list. Every key a selector sets must match. operation_id is the source’s own operationId (for Google Discovery, the method id such as drive.files.list). operation_id and tag accept *. path is a glob over the source path: * stays within one segment and ** spans segments. Quote a path in YAML when it contains { or *.
excludeSelectors removed from the selection.
unexposedskip (default) leaves unselected operations out of the AIR and prunes every component schema only they reach, before $ref dereferencing. compile keeps them in the AIR, compiled, but never approvable.
schema_boundsHow operation schemas are materialized; see Schema bounds.
approveA named reviewer and reason approving some or all selected operations at compile time. It goes through the manifest’s state: approved channel, so every gate a manifest approval meets still applies: an unresolvable idempotency carrier, a query-language passthrough, or an incoherent auth contract leaves the operation blocked.
sourcePin the reviewed contract; a compile against any other is refused, and the pin is recorded in service.source.profile.source. url says where it is published (recorded, never fetched; it needs a digest beside it). sha256 is the sha256 of the entrypoint file’s bytes. content_sha256 is the sha256 of its parsed document as canonical JSON, for a publisher whose bytes change between downloads while the document does not (Google Discovery reorders its keys on every response); the refusal prints the value to pin. digest is the snapshot’s sourceHash, which also covers the file names.

Keys are strict. anvil schema profile prints the JSON Schema for editor validation. The profile digest is the sha256 of its parsed, key-sorted JSON, so reformatting the file does not change it.

Terminal window
anvil compile jira.json --profile jira.profile.yaml --manifest anvil.yaml \
--out generated/jira
# Compiled 39 operations ...
# profile jira-issues (sha256:...): 39 of 619 source operations exposed; the rest skipped.
anvil inspect generated/jira
anvil approve generated/jira --profile --reviewer alice@example.com --dry-run
anvil approve generated/jira --profile --reviewer alice@example.com
anvil sdk generated/jira --lang python --out sdk
anvil simulate serve --contract generated/jira --port 0

anvil approve --profile approves every selected operation that is not yet approved and not blocked, names the blocked ones, and appends one record to .anvil/approvals.jsonl whose note carries the profile id and digest. It requires --reviewer. Approving an operation outside the profile is refused, by anvil approve and by a manifest state: approved (the compile withdraws it with a profile/approval_outside_profile warning). Generated artifacts and the simulator expose approved operations only, so the exposed surface is always a subset of the profile.

Reviewed profiles and manifests for Jira, Confluence, Slack, Google Drive, and Microsoft Graph are in examples/profiles/, each pinned to its vendor spec by URL and sha256.

Some vendor contracts need a manifest before any operation can be approved. Jira and Confluence declare basic auth or OAuth as alternatives, which blocks every operation until a service-level auth.type chooses one (see MANIFEST.md).

A whole-source profile is select: all. It compiles every operation, which for a very large contract is slow and produces a large AIR; lower schema_bounds for it (see the measurements below).

A compile materializes each operation’s request and response schema into a self-contained, $ref-free schema. Without a profile the historical bounds apply: one named-schema hop, and a 4,000-node expansion budget. A profile compile uses these defaults instead:

BoundProfile defaultMeaning
max_ref_depth3The deepest named-schema hop chain tried
max_schema_nodes2000The largest materialized tree accepted
inherit_all_oftrueA $ref directly inside allOf is inheritance and spends no hop; the base’s discriminator mapping is dropped

Each schema gets the deepest depth up to max_ref_depth whose tree fits in max_schema_nodes; if none does, one hop truncated at the node budget. On Microsoft Graph, GET /users then shows the collection envelope and each user with its inherited directoryObject and entity fields, and GET /users/{user-id} stays at one hop instead of expanding every navigation property. A cycle is always cut to a stub that names the schema.

A JSON document is read with JSON.parse, and a YAML document with js-yaml under a schema that resolves scalars exactly as YAML 1.2 core does. Documents with anchors or merge keys, and documents either fast reader rejects, go through the yaml document parser, which reports errors with a line and column.

Workload: the published specs listed below, fetched 2026-09-26. Method: one run each of anvil compile from a spec file into an empty workspace on a 4-core Linux container with 15GB of memory, Node 22; wall time and peak RSS of the process. “Before” is revision 1b448fc with no profile; “after” adds the profile named. The profile rows compile 20 to 60 operations.

Spec (operations)Before, whole sourceAfter, profileAfter, select: all
Jira Cloud platform v3, 2.5MB JSON (619)7.6s, 760MB; 605 operations blocked39 operations: 2.9s, 385MB12.5s, 1.0GB
Confluence v2, 0.6MB JSON (218)3.3s, 430MB33 operations: 2.0s, 297MBnot measured
Slack Web API, Swagger 2.0, 1.2MB JSON (174)3.5s, 417MB29 operations: 2.0s, 282MBnot measured
Google Drive v3 Discovery, 0.3MB JSON (64)2.4s, 353MB28 operations: 2.3s, 325MBnot measured
Microsoft Graph v1.0, 44MB YAML (17,870)failed after 325s at 8.0GB (file name too long)46 operations: 12s, 1.6GB129s, 9.0GB, with max_ref_depth: 1 and max_schema_nodes: 1000

Parsing alone, on the same machine: the Jira JSON took 1.6s with the yaml document parser and 0.02s with JSON.parse; the Graph YAML took 22s and 1.5GB with the yaml package and 2.6s and 0.4GB with js-yaml.

For each profile bundle, anvil sdk --lang python took 1.3s to 3.1s, anvil simulate serve printed its URL within 1.6s to 3.6s at 272MB to 506MB peak RSS, and every approved read answered over HTTP. The Jira select: all compile is slower than the unprofiled one because it materializes deeper schemas under the profile bounds (a 26MB AIR instead of 7.7MB). The Graph select: all compile needs a machine with more than 9GB of memory; at the profile default bounds its AIR passes the 512MB string limit of the JavaScript engine, and the compile says to lower the bounds.

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.

A request body’s content type is carried verbatim into AIR. Where a source declares several, JSON is preferred, then any other type the runtime encodes (application/x-www-form-urlencoded, multipart/form-data), then the first declared. A body in any other content type compiles with the body_content_type_unsupported diagnostic and is held for review, because the runtime refuses to send it — see request bodies for what each encoding puts on the wire, including how a multipart file field (type: string, format: binary) is supplied as base64 and sent as bytes.

A parameter’s style and explode are carried into AIR only when the source declares them; an undeclared one is left absent and serialized by OpenAPI’s per-location default at call time. The serialization table names what each style puts on the wire and which value shapes are refused.

Anvil maps each root field to one operation:

  • Query fields are reads;
  • Mutation fields are mutations; and
  • Subscription fields 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: 120

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

The response schema and the compiled query document are two projections of one selection tree, so the schema never promises a field the document does not ask for:

  • A union is selected through an inline fragment per member (... on Product { … }), and its schema is a oneOf of the members. An interface is selected as its own fields plus a fragment per implementation, and its schema is a oneOf of the implementations. Every such object carries __typename.
  • A field that takes a required argument is left out of both the document and the schema, because selecting it would mean inventing argument values. graphql_field_omitted_required_args names each such field, once per operation. Expose it through a root field of its own, or give the argument a default in the SDL.
  • The selection is bounded: four levels deep, and never re-entering a type already on the path. Where it stops, the document selects only __typename, and graphql_selection_truncated names each position. The response schema renders such a position as the TypenameOnly component wherever it is rendered inline; past the inline bound it falls back to the type’s component and the diagnostic says so.

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:

Terminal window
anvil source add path/to/graphql-schema-directory
anvil compile --source <snapshot-id> --entrypoint schema.graphql --out generated/service

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

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.

Each WSDL 1.1 portType operation becomes a POST-shaped operation. The adapter understands the common document/literal XSD subset:

  • global elements;
  • complexType with sequence, all, or choice;
  • simpleType enumeration restrictions;
  • complexContent extension;
  • element references;
  • minOccurs and maxOccurs; and
  • common XSD scalar types.

An xsd:choice lowers to optional members under a oneOf that admits exactly one branch, so an agent cannot satisfy the request schema by sending every branch at once. A sequence branch requires its non-optional elements, a nested choice flattens into the outer one, and a choice with minOccurs="0" admits the empty case. wsdl_choice_lowered names each type this happened to, once. The SOAP envelope carries only the branch that was sent.

WSDL 2.0 is not lowered. A <description> document in the WSDL 2.0 namespace is detected and labelled 2.0, then refused with the error-level wsdl_version_unsupported diagnostic and zero operations, rather than silently compiled into an empty service. Supply a WSDL 1.1 description of the same service.

Operation names provide conservative effect hints; they do not prove idempotency. See Add the safety facts a WSDL leaves out for a complete example.

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.

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:

DeclarationLowers toEffect
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 bodyMutation
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:HttpMethodPOST /NameMutation, 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.

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.

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.

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:

ClassificationMeaningExample estate
resource_grammarNouns in the path; HTTP methods carry the verbZendesk, GitHub, Stripe
rpc_plainVerbs as plain terminal segments; method mix collapsed onto POSTPlaid (POST /transactions/get)
rpc_dottedDotted RPC method segments the URL itself declaresSlack (/chat.postMessage)
adapter_loweredA protocol adapter (WSDL/GraphQL/protobuf/MCP) wrote the paths; the shape is declared by constructionNetSuite’s lowered /NetSuitePortType/get
ambiguousThe 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.

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.http annotation, 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 over graphql-sse — chunked HTTP, which every runtime already reads. A service that speaks only graphql-ws needs 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.

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:

Terminal window
anvil estate support --json

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

For every format:

Terminal window
anvil status generated/service
anvil inspect generated/service
anvil assess generated/service
anvil lint generated/service

Treat adapter output as a candidate contract. Resolve missing business meaning in an Anvil manifest, recompile, and approve only the operations you have inspected.